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>.
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 = DEFAULT_SHOW_WARNINGS;
100    showDeprecationWarnings = DEFAULT_SHOW_DEPRECATION_WARNINGS;
101    failOnWarnings = DEFAULT_FAIL_ON_WARNINGS;
102    compilationMode = DEFAULT_COMPILATION_MODE;
103    locale = DEFAULT_LOCALE;
104    logCharset = DEFAULT_LOG_CHARSET;
105    previewFeatures = DEFAULT_PREVIEW_FEATURES;
106    verbose = DEFAULT_VERBOSE;
107    diagnosticLoggingMode = DEFAULT_DIAGNOSTIC_LOGGING_MODE;
108    fixJvmModulePathMismatch = DEFAULT_FIX_JVM_MODULE_PATH_MISMATCH;
109    inheritClassPath = DEFAULT_INHERIT_CLASS_PATH;
110    inheritModulePath = DEFAULT_INHERIT_MODULE_PATH;
111    inheritSystemModulePath = DEFAULT_INHERIT_SYSTEM_MODULE_PATH;
112    fileManagerLoggingMode = DEFAULT_FILE_MANAGER_LOGGING_MODE;
113    annotationProcessorDiscovery = 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  @Nullable
278  @Override
279  public String getRelease() {
280    return release;
281  }
282
283  @Override
284  public AbstractJctCompiler release(@Nullable String release) {
285    this.release = release;
286
287    if (release != null) {
288      source = null;
289      target = null;
290    }
291
292    return this;
293  }
294
295  @Nullable
296  @Override
297  public String getSource() {
298    return source;
299  }
300
301  @Override
302  public AbstractJctCompiler source(@Nullable String source) {
303    this.source = source;
304    if (source != null) {
305      release = null;
306    }
307    return this;
308  }
309
310  @Nullable
311  @Override
312  public String getTarget() {
313    return target;
314  }
315
316  @Override
317  public AbstractJctCompiler target(@Nullable String target) {
318    this.target = target;
319    if (target != null) {
320      release = null;
321    }
322    return this;
323  }
324
325  @Override
326  public boolean isFixJvmModulePathMismatch() {
327    return fixJvmModulePathMismatch;
328  }
329
330  @Override
331  public AbstractJctCompiler fixJvmModulePathMismatch(boolean fixJvmModulePathMismatch) {
332    this.fixJvmModulePathMismatch = fixJvmModulePathMismatch;
333    return this;
334  }
335
336  @Override
337  public boolean isInheritClassPath() {
338    return inheritClassPath;
339  }
340
341  @Override
342  public AbstractJctCompiler inheritClassPath(boolean inheritClassPath) {
343    this.inheritClassPath = inheritClassPath;
344    return this;
345  }
346
347  @Override
348  public boolean isInheritModulePath() {
349    return inheritModulePath;
350  }
351
352  @Override
353  public AbstractJctCompiler inheritModulePath(boolean inheritModulePath) {
354    this.inheritModulePath = inheritModulePath;
355    return this;
356  }
357
358  @Override
359  public boolean isInheritSystemModulePath() {
360    return inheritSystemModulePath;
361  }
362
363  @Override
364  public AbstractJctCompiler inheritSystemModulePath(boolean inheritSystemModulePath) {
365    this.inheritSystemModulePath = inheritSystemModulePath;
366    return this;
367  }
368
369  @Override
370  public Locale getLocale() {
371    return locale;
372  }
373
374  @Override
375  public AbstractJctCompiler locale(Locale locale) {
376    requireNonNull(locale, "locale");
377    this.locale = locale;
378    return this;
379  }
380
381  @Override
382  public Charset getLogCharset() {
383    return logCharset;
384  }
385
386  @Override
387  public AbstractJctCompiler logCharset(Charset logCharset) {
388    requireNonNull(logCharset, "logCharset");
389    this.logCharset = logCharset;
390    return this;
391  }
392
393  @Override
394  public LoggingMode getFileManagerLoggingMode() {
395    return fileManagerLoggingMode;
396  }
397
398  @Override
399  public AbstractJctCompiler fileManagerLoggingMode(LoggingMode fileManagerLoggingMode) {
400    requireNonNull(fileManagerLoggingMode, "fileManagerLoggingMode");
401    this.fileManagerLoggingMode = fileManagerLoggingMode;
402    return this;
403  }
404
405  @Override
406  public LoggingMode getDiagnosticLoggingMode() {
407    return diagnosticLoggingMode;
408  }
409
410  @Override
411  public AbstractJctCompiler diagnosticLoggingMode(LoggingMode diagnosticLoggingMode) {
412    requireNonNull(diagnosticLoggingMode, "diagnosticLoggingMode");
413    this.diagnosticLoggingMode = diagnosticLoggingMode;
414    return this;
415  }
416
417  @Override
418  public AnnotationProcessorDiscovery getAnnotationProcessorDiscovery() {
419    return annotationProcessorDiscovery;
420  }
421
422  @Override
423  public AbstractJctCompiler annotationProcessorDiscovery(
424      AnnotationProcessorDiscovery annotationProcessorDiscovery
425  ) {
426    requireNonNull(annotationProcessorDiscovery, 
427        "annotationProcessorDiscovery");
428    this.annotationProcessorDiscovery = annotationProcessorDiscovery;
429    return this;
430  }
431
432  @Override
433  public Set<DebuggingInfo> getDebuggingInfo() {
434    return debuggingInfo;
435  }
436
437  @Override
438  public JctCompiler debuggingInfo(Set<DebuggingInfo> debuggingInfo) {
439    requireNonNullValues(debuggingInfo, "debuggingInfo");
440    this.debuggingInfo = Set.copyOf(debuggingInfo);
441    return this;
442  }
443
444  @Override
445  public boolean isParameterInfoEnabled() {
446    return parameterInfoEnabled;
447  }
448
449  @Override
450  public JctCompiler parameterInfoEnabled(boolean parameterInfoEnabled) {
451    this.parameterInfoEnabled = parameterInfoEnabled;
452    return this;
453  }
454
455  /**
456   * Get the string representation of the compiler.
457   *
458   * @return the string representation of the compiler.
459   */
460  @Override
461  public final String toString() {
462    // This returns the compiler name to simplify parameterization naming in @JavacCompilerTest
463    // parameterized tests.
464    return name;
465  }
466
467  /**
468   * Get the flag builder factory to use for building flags.
469   *
470   * @return the factory.
471   */
472  public abstract JctFlagBuilderFactory getFlagBuilderFactory();
473
474  /**
475   * Get the JSR-199 compiler factory to use for initialising an internal compiler.
476   *
477   * @return the factory.
478   */
479  public abstract Jsr199CompilerFactory getCompilerFactory();
480
481  /**
482   * Get the file manager factory to use for building AbstractJctCompiler file manager during
483   * compilation.
484   *
485   * <p>Since v1.1.0, this method has provided a default implementation. Before this, it was
486   * abstract. The default implementation calls
487   * {@link JctFileManagers#newJctFileManagerFactory(JctCompiler)}.
488   *
489   * @return the factory.
490   */
491  public JctFileManagerFactory getFileManagerFactory() {
492    return JctFileManagers.newJctFileManagerFactory(this);
493  }
494
495  /**
496   * Get the compilation factory to use for building a compilation.
497   *
498   * <p>By default, this uses a common internal implementation that is designed to work with
499   * compilers that have interfaces the same as, and behave the same as Javac.
500   *
501   * <p>Some obscure compiler implementations with potentially satanic rituals for initialising
502   * and configuring components correctly may need to provide a custom implementation here instead.
503   * In this case, this method should be overridden. Base classes are not provided for you to extend
504   * in this case as this is usually not something you want to be doing. Instead, you should
505   * implement {@link JctCompilationFactory} directly.
506   *
507   * @return the compilation factory.
508   */
509  public JctCompilationFactory getCompilationFactory() {
510    return new JctCompilationFactoryImpl(this);
511  }
512
513  /**
514   * {@inheritDoc}
515   *
516   * @return the default release version to use when no version is specified by the user.
517   */
518  @Override
519  public abstract String getDefaultRelease();
520
521  /**
522   * Build the list of flags from this compiler object using the flag builder.
523   *
524   * <p>Implementations should not need to override this unless there is a special edge case
525   * that needs configuring differently. This is exposed to assist in these kinds of cases.
526   *
527   * @param flagBuilder the flag builder to apply the flag configuration to.
528   * @return the string flags to use.
529   */
530  protected List<String> buildFlags(JctFlagBuilder flagBuilder) {
531    return flagBuilder
532        .annotationProcessorOptions(annotationProcessorOptions)
533        .showDeprecationWarnings(showDeprecationWarnings)
534        .failOnWarnings(failOnWarnings)
535        .compilerOptions(compilerOptions)
536        .previewFeatures(previewFeatures)
537        .release(release)
538        .source(source)
539        .target(target)
540        .verbose(verbose)
541        .showWarnings(showWarnings)
542        .debuggingInfo(debuggingInfo)
543        .parameterInfoEnabled(parameterInfoEnabled)
544        .build();
545  }
546
547  @SuppressWarnings("ThrowFromFinallyBlock")
548  private JctCompilation performCompilation(
549        Workspace workspace, 
550        @Nullable Collection<String> classNames
551  ) {
552    var fileManagerFactory = getFileManagerFactory();
553    var flagBuilderFactory = getFlagBuilderFactory();
554    var compilerFactory = getCompilerFactory();
555    var compilationFactory = getCompilationFactory();
556    var flags = buildFlags(flagBuilderFactory.createFlagBuilder());
557    var compiler = compilerFactory.createCompiler();
558    var fileManager = fileManagerFactory.createFileManager(workspace);
559
560    // Any internal exceptions should be rethrown as a JctCompilerException by the
561    // compilation factory, so there is nothing else to worry about here.
562    // Likewise, do not catch IOException on the compilation process, as it may hide
563    // bugs.
564    //
565    // The try-finally-try-catch-rethrow ensures we only catch IOExceptions during
566    // the file manager closure, where it is a bug.
567    
568    try {
569      return compilationFactory.createCompilation(flags, fileManager, compiler, classNames);
570    } finally {
571      try {
572        fileManager.close();
573      } catch (IOException ex) {
574        throw new JctCompilerException(
575            "Failed to close file manager. This is probably a bug, so please report it.",
576            ex
577        );
578      }
579    }
580  }
581}