001/*
002 * Copyright (C) 2022 - 2024, the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *    http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.github.ascopes.jct.compilers;
017
018import static io.github.ascopes.jct.utils.IterableUtils.requireNonNullValues;
019import static java.util.Objects.requireNonNull;
020
021import io.github.ascopes.jct.compilers.impl.JctCompilationFactoryImpl;
022import io.github.ascopes.jct.compilers.impl.JctCompilationImpl;
023import io.github.ascopes.jct.ex.JctCompilerException;
024import io.github.ascopes.jct.filemanagers.AnnotationProcessorDiscovery;
025import io.github.ascopes.jct.filemanagers.JctFileManagerFactory;
026import io.github.ascopes.jct.filemanagers.JctFileManagers;
027import io.github.ascopes.jct.filemanagers.LoggingMode;
028import io.github.ascopes.jct.workspaces.Workspace;
029import java.io.IOException;
030import java.nio.charset.Charset;
031import java.util.ArrayList;
032import java.util.Collection;
033import java.util.List;
034import java.util.Locale;
035import java.util.Set;
036import javax.annotation.processing.Processor;
037import org.apiguardian.api.API;
038import org.apiguardian.api.API.Status;
039import org.jspecify.annotations.Nullable;
040
041/**
042 * Common functionality for a compiler that can be overridden and that produces a
043 * {@link JctCompilationImpl} as the compilation result.
044 *
045 * <p>Implementations should extend this class and override anything they require.
046 * In most cases, you should not need to override anything other than the constructor.
047 *
048 * <p>This class is <strong>not thread-safe</strong> thread-safe!
049 *
050 * <p>If you wish to create a common set of configuration settings for instances of
051 * this class, you should consider writing a custom {@link JctCompilerConfigurer} object to apply
052 * the desired operations, and then apply it to instances of this class using
053 * {@link #configure(JctCompilerConfigurer)}.
054 *
055 * @author Ashley Scopes
056 * @since 0.0.1
057 */
058@API(since = "0.0.1", status = Status.STABLE)
059public abstract class AbstractJctCompiler implements JctCompiler {
060
061  private final List<Processor> annotationProcessors;
062  private final List<String> annotationProcessorOptions;
063  private final List<String> compilerOptions;
064
065  private String name;
066  private boolean showWarnings;
067  private boolean showDeprecationWarnings;
068  private boolean failOnWarnings;
069  private CompilationMode compilationMode;
070  private Locale locale;
071  private Charset logCharset;
072  private boolean verbose;
073  private boolean previewFeatures;
074  private LoggingMode diagnosticLoggingMode;
075  private boolean fixJvmModulePathMismatch;
076  private boolean inheritClassPath;
077  private boolean inheritModulePath;
078  private boolean inheritSystemModulePath;
079  private LoggingMode fileManagerLoggingMode;
080  private AnnotationProcessorDiscovery annotationProcessorDiscovery;
081  private Set<DebuggingInfo> debuggingInfo;
082  private boolean parameterInfoEnabled;
083
084  private @Nullable String release;
085  private @Nullable String source;
086  private @Nullable String target;
087
088  /**
089   * Initialize this compiler.
090   *
091   * @param defaultName the printable default name to use for the compiler.
092   */
093  protected AbstractJctCompiler(String defaultName) {
094    name = requireNonNull(defaultName, "name");
095
096    annotationProcessors = new ArrayList<>();
097    annotationProcessorOptions = new ArrayList<>();
098    compilerOptions = new ArrayList<>();
099    showWarnings = JctCompiler.DEFAULT_SHOW_WARNINGS;
100    showDeprecationWarnings = JctCompiler.DEFAULT_SHOW_DEPRECATION_WARNINGS;
101    failOnWarnings = JctCompiler.DEFAULT_FAIL_ON_WARNINGS;
102    compilationMode = JctCompiler.DEFAULT_COMPILATION_MODE;
103    locale = JctCompiler.DEFAULT_LOCALE;
104    logCharset = JctCompiler.DEFAULT_LOG_CHARSET;
105    previewFeatures = JctCompiler.DEFAULT_PREVIEW_FEATURES;
106    verbose = JctCompiler.DEFAULT_VERBOSE;
107    diagnosticLoggingMode = JctCompiler.DEFAULT_DIAGNOSTIC_LOGGING_MODE;
108    fixJvmModulePathMismatch = JctCompiler.DEFAULT_FIX_JVM_MODULE_PATH_MISMATCH;
109    inheritClassPath = JctCompiler.DEFAULT_INHERIT_CLASS_PATH;
110    inheritModulePath = JctCompiler.DEFAULT_INHERIT_MODULE_PATH;
111    inheritSystemModulePath = JctCompiler.DEFAULT_INHERIT_SYSTEM_MODULE_PATH;
112    fileManagerLoggingMode = JctCompiler.DEFAULT_FILE_MANAGER_LOGGING_MODE;
113    annotationProcessorDiscovery = JctCompiler.DEFAULT_ANNOTATION_PROCESSOR_DISCOVERY;
114    debuggingInfo = DEFAULT_DEBUGGING_INFO;
115    parameterInfoEnabled = DEFAULT_PARAMETER_INFO_ENABLED;
116
117    // If none of these are overridden then we assume the defaults instead.
118    release = null;
119    source = null;
120    target = null;
121  }
122
123  @Override
124  public JctCompilation compile(Workspace workspace) {
125    return performCompilation(workspace, null);
126  }
127
128  @Override
129  public JctCompilation compile(Workspace workspace, Collection<String> classNames) {
130    // There is no reason to invoke this overload with null values, so
131    // prevent this.
132    requireNonNullValues(classNames, "classNames");
133    return performCompilation(workspace, classNames);
134  }
135
136  @Override
137  public final <E extends Exception> AbstractJctCompiler configure(
138      JctCompilerConfigurer<E> configurer
139  ) throws E {
140    requireNonNull(configurer, "configurer");
141    configurer.configure(this);
142    return this;
143  }
144
145  @Override
146  public String getName() {
147    return name;
148  }
149
150  @Override
151  public AbstractJctCompiler name(String name) {
152    requireNonNull(name, "name");
153    this.name = name;
154    return this;
155  }
156
157  @Override
158  public boolean isVerbose() {
159    return verbose;
160  }
161
162  @Override
163  public AbstractJctCompiler verbose(boolean enabled) {
164    verbose = enabled;
165    return this;
166  }
167
168  @Override
169  public boolean isPreviewFeatures() {
170    return previewFeatures;
171  }
172
173  @Override
174  public AbstractJctCompiler previewFeatures(boolean enabled) {
175    previewFeatures = enabled;
176    return this;
177  }
178
179  @Override
180  public boolean isShowWarnings() {
181    return showWarnings;
182  }
183
184  @Override
185  public AbstractJctCompiler showWarnings(boolean enabled) {
186    showWarnings = enabled;
187    return this;
188  }
189
190  @Override
191  public boolean isShowDeprecationWarnings() {
192    return showDeprecationWarnings;
193  }
194
195  @Override
196  public AbstractJctCompiler showDeprecationWarnings(boolean enabled) {
197    showDeprecationWarnings = enabled;
198    return this;
199  }
200
201  @Override
202  public boolean isFailOnWarnings() {
203    return failOnWarnings;
204  }
205
206  @Override
207  public AbstractJctCompiler failOnWarnings(boolean enabled) {
208    failOnWarnings = enabled;
209    return this;
210  }
211
212  @Override
213  public CompilationMode getCompilationMode() {
214    return compilationMode;
215  }
216
217  @Override
218  public AbstractJctCompiler compilationMode(CompilationMode compilationMode) {
219    this.compilationMode = compilationMode;
220    return this;
221  }
222
223  @Override
224  public List<String> getAnnotationProcessorOptions() {
225    return List.copyOf(annotationProcessorOptions);
226  }
227
228  @Override
229  public AbstractJctCompiler addAnnotationProcessorOptions(
230      Iterable<String> annotationProcessorOptions
231  ) {
232    requireNonNullValues(annotationProcessorOptions, "annotationProcessorOptions");
233    annotationProcessorOptions.forEach(this.annotationProcessorOptions::add);
234    return this;
235  }
236
237  @Override
238  public List<Processor> getAnnotationProcessors() {
239    return List.copyOf(annotationProcessors);
240  }
241
242  @Override
243  public AbstractJctCompiler addAnnotationProcessors(
244      Iterable<? extends Processor> annotationProcessors
245  ) {
246    requireNonNullValues(annotationProcessors, "annotationProcessors");
247    annotationProcessors.forEach(this.annotationProcessors::add);
248
249    return this;
250  }
251
252  @Override
253  public List<String> getCompilerOptions() {
254    return List.copyOf(compilerOptions);
255  }
256
257  @Override
258  public AbstractJctCompiler addCompilerOptions(Iterable<String> compilerOptions) {
259    requireNonNullValues(compilerOptions, "compilerOptions");
260    compilerOptions.forEach(this.compilerOptions::add);
261    return this;
262  }
263
264  @Override
265  public String getEffectiveRelease() {
266    if (release != null) {
267      return release;
268    }
269
270    if (target != null) {
271      return target;
272    }
273
274    return getDefaultRelease();
275  }
276
277  @Override
278  public String getRelease() {
279    return release;
280  }
281
282  @Override
283  public AbstractJctCompiler release(@Nullable String release) {
284    this.release = release;
285
286    if (release != null) {
287      source = null;
288      target = null;
289    }
290
291    return this;
292  }
293
294  @Override
295  public String getSource() {
296    return source;
297  }
298
299  @Override
300  public AbstractJctCompiler source(@Nullable String source) {
301    this.source = source;
302    if (source != null) {
303      release = null;
304    }
305    return this;
306  }
307
308  @Override
309  public String getTarget() {
310    return target;
311  }
312
313  @Override
314  public AbstractJctCompiler target(@Nullable String target) {
315    this.target = target;
316    if (target != null) {
317      release = null;
318    }
319    return this;
320  }
321
322  @Override
323  public boolean isFixJvmModulePathMismatch() {
324    return fixJvmModulePathMismatch;
325  }
326
327  @Override
328  public AbstractJctCompiler fixJvmModulePathMismatch(boolean fixJvmModulePathMismatch) {
329    this.fixJvmModulePathMismatch = fixJvmModulePathMismatch;
330    return this;
331  }
332
333  @Override
334  public boolean isInheritClassPath() {
335    return inheritClassPath;
336  }
337
338  @Override
339  public AbstractJctCompiler inheritClassPath(boolean inheritClassPath) {
340    this.inheritClassPath = inheritClassPath;
341    return this;
342  }
343
344  @Override
345  public boolean isInheritModulePath() {
346    return inheritModulePath;
347  }
348
349  @Override
350  public AbstractJctCompiler inheritModulePath(boolean inheritModulePath) {
351    this.inheritModulePath = inheritModulePath;
352    return this;
353  }
354
355  @Override
356  public boolean isInheritSystemModulePath() {
357    return inheritSystemModulePath;
358  }
359
360  @Override
361  public AbstractJctCompiler inheritSystemModulePath(boolean inheritSystemModulePath) {
362    this.inheritSystemModulePath = inheritSystemModulePath;
363    return this;
364  }
365
366  @Override
367  public Locale getLocale() {
368    return locale;
369  }
370
371  @Override
372  public AbstractJctCompiler locale(Locale locale) {
373    requireNonNull(locale, "locale");
374    this.locale = locale;
375    return this;
376  }
377
378  @Override
379  public Charset getLogCharset() {
380    return logCharset;
381  }
382
383  @Override
384  public AbstractJctCompiler logCharset(Charset logCharset) {
385    requireNonNull(logCharset, "logCharset");
386    this.logCharset = logCharset;
387    return this;
388  }
389
390  @Override
391  public LoggingMode getFileManagerLoggingMode() {
392    return fileManagerLoggingMode;
393  }
394
395  @Override
396  public AbstractJctCompiler fileManagerLoggingMode(LoggingMode fileManagerLoggingMode) {
397    requireNonNull(fileManagerLoggingMode, "fileManagerLoggingMode");
398    this.fileManagerLoggingMode = fileManagerLoggingMode;
399    return this;
400  }
401
402  @Override
403  public LoggingMode getDiagnosticLoggingMode() {
404    return diagnosticLoggingMode;
405  }
406
407  @Override
408  public AbstractJctCompiler diagnosticLoggingMode(LoggingMode diagnosticLoggingMode) {
409    requireNonNull(diagnosticLoggingMode, "diagnosticLoggingMode");
410    this.diagnosticLoggingMode = diagnosticLoggingMode;
411    return this;
412  }
413
414  @Override
415  public AnnotationProcessorDiscovery getAnnotationProcessorDiscovery() {
416    return annotationProcessorDiscovery;
417  }
418
419  @Override
420  public AbstractJctCompiler annotationProcessorDiscovery(
421      AnnotationProcessorDiscovery annotationProcessorDiscovery
422  ) {
423    requireNonNull(annotationProcessorDiscovery, 
424        "annotationProcessorDiscovery");
425    this.annotationProcessorDiscovery = annotationProcessorDiscovery;
426    return this;
427  }
428
429  @Override
430  public Set<DebuggingInfo> getDebuggingInfo() {
431    return debuggingInfo;
432  }
433
434  @Override
435  public JctCompiler debuggingInfo(Set<DebuggingInfo> debuggingInfo) {
436    requireNonNullValues(debuggingInfo, "debuggingInfo");
437    this.debuggingInfo = Set.copyOf(debuggingInfo);
438    return this;
439  }
440
441  @Override
442  public boolean isParameterInfoEnabled() {
443    return parameterInfoEnabled;
444  }
445
446  @Override
447  public JctCompiler parameterInfoEnabled(boolean parameterInfoEnabled) {
448    this.parameterInfoEnabled = parameterInfoEnabled;
449    return this;
450  }
451
452  /**
453   * Get the string representation of the compiler.
454   *
455   * @return the string representation of the compiler.
456   */
457  @Override
458  public final String toString() {
459    // This returns the compiler name to simplify parameterization naming in @JavacCompilerTest
460    // parameterized tests.
461    return name;
462  }
463
464  /**
465   * Get the flag builder factory to use for building flags.
466   *
467   * @return the factory.
468   */
469  public abstract JctFlagBuilderFactory getFlagBuilderFactory();
470
471  /**
472   * Get the JSR-199 compiler factory to use for initialising an internal compiler.
473   *
474   * @return the factory.
475   */
476  public abstract Jsr199CompilerFactory getCompilerFactory();
477
478  /**
479   * Get the file manager factory to use for building AbstractJctCompiler file manager during
480   * compilation.
481   *
482   * <p>Since v1.1.0, this method has provided a default implementation. Before this, it was
483   * abstract. The default implementation calls
484   * {@link JctFileManagers#newJctFileManagerFactory(JctCompiler)}.
485   *
486   * @return the factory.
487   */
488  public JctFileManagerFactory getFileManagerFactory() {
489    return JctFileManagers.newJctFileManagerFactory(this);
490  }
491
492  /**
493   * Get the compilation factory to use for building a compilation.
494   *
495   * <p>By default, this uses a common internal implementation that is designed to work with
496   * compilers that have interfaces the same as, and behave the same as Javac.
497   *
498   * <p>Some obscure compiler implementations with potentially satanic rituals for initialising
499   * and configuring components correctly may need to provide a custom implementation here instead.
500   * In this case, this method should be overridden. Base classes are not provided for you to extend
501   * in this case as this is usually not something you want to be doing. Instead, you should
502   * implement {@link JctCompilationFactory} directly.
503   *
504   * @return the compilation factory.
505   */
506  public JctCompilationFactory getCompilationFactory() {
507    return new JctCompilationFactoryImpl(this);
508  }
509
510  /**
511   * {@inheritDoc}
512   *
513   * @return the default release version to use when no version is specified by the user.
514   */
515  @Override
516  public abstract String getDefaultRelease();
517
518  /**
519   * Build the list of flags from this compiler object using the flag builder.
520   *
521   * <p>Implementations should not need to override this unless there is a special edge case
522   * that needs configuring differently. This is exposed to assist in these kinds of cases.
523   *
524   * @param flagBuilder the flag builder to apply the flag configuration to.
525   * @return the string flags to use.
526   */
527  protected List<String> buildFlags(JctFlagBuilder flagBuilder) {
528    return flagBuilder
529        .annotationProcessorOptions(annotationProcessorOptions)
530        .showDeprecationWarnings(showDeprecationWarnings)
531        .failOnWarnings(failOnWarnings)
532        .compilerOptions(compilerOptions)
533        .previewFeatures(previewFeatures)
534        .release(release)
535        .source(source)
536        .target(target)
537        .verbose(verbose)
538        .showWarnings(showWarnings)
539        .debuggingInfo(debuggingInfo)
540        .parameterInfoEnabled(parameterInfoEnabled)
541        .build();
542  }
543
544  @SuppressWarnings("ThrowFromFinallyBlock")
545  private JctCompilation performCompilation(
546        Workspace workspace, 
547        @Nullable Collection<String> classNames
548  ) {
549    var fileManagerFactory = getFileManagerFactory();
550    var flagBuilderFactory = getFlagBuilderFactory();
551    var compilerFactory = getCompilerFactory();
552    var compilationFactory = getCompilationFactory();
553    var flags = buildFlags(flagBuilderFactory.createFlagBuilder());
554    var compiler = compilerFactory.createCompiler();
555    var fileManager = fileManagerFactory.createFileManager(workspace);
556
557    // Any internal exceptions should be rethrown as a JctCompilerException by the
558    // compilation factory, so there is nothing else to worry about here.
559    // Likewise, do not catch IOException on the compilation process, as it may hide
560    // bugs.
561    //
562    // The try-finally-try-catch-rethrow ensures we only catch IOExceptions during
563    // the file manager closure, where it is a bug.
564    
565    try {
566      return compilationFactory.createCompilation(flags, fileManager, compiler, classNames);
567    } finally {
568      try {
569        fileManager.close();
570      } catch (IOException ex) {
571        throw new JctCompilerException(
572            "Failed to close file manager. This is probably a bug, so please report it.",
573            ex
574        );
575      }
576    }
577  }
578}