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.jspecify.annotations.Nullable;
038
039/**
040 * Common functionality for a compiler that can be overridden and that produces a
041 * {@link JctCompilationImpl} as the compilation result.
042 *
043 * <p>Implementations should extend this class and override anything they require.
044 * In most cases, you should not need to override anything other than the constructor.
045 *
046 * <p>This class is <strong>not thread-safe</strong>.
047 *
048 * <p>If you wish to create a common set of configuration settings for instances of
049 * this class, you should consider writing a custom {@link JctCompilerConfigurer} object to apply
050 * the desired operations, and then apply it to instances of this class using
051 * {@link #configure(JctCompilerConfigurer)}.
052 *
053 * @author Ashley Scopes
054 * @since 0.0.1
055 */
056public abstract class AbstractJctCompiler implements JctCompiler {
057
058  private final List<Processor> annotationProcessors;
059  private final List<String> annotationProcessorOptions;
060  private final List<String> compilerOptions;
061
062  private String name;
063  private boolean showWarnings;
064  private boolean showDeprecationWarnings;
065  private boolean failOnWarnings;
066  private CompilationMode compilationMode;
067  private Locale locale;
068  private Charset logCharset;
069  private boolean verbose;
070  private boolean previewFeatures;
071  private LoggingMode diagnosticLoggingMode;
072  private boolean fixJvmModulePathMismatch;
073  private boolean inheritClassPath;
074  private boolean inheritModulePath;
075  private boolean inheritSystemModulePath;
076  private LoggingMode fileManagerLoggingMode;
077  private AnnotationProcessorDiscovery annotationProcessorDiscovery;
078  private Set<DebuggingInfo> debuggingInfo;
079  private boolean parameterInfoEnabled;
080
081  private @Nullable String release;
082  private @Nullable String source;
083  private @Nullable String target;
084
085  /**
086   * Initialize this compiler.
087   *
088   * @param defaultName the printable default name to use for the compiler.
089   */
090  protected AbstractJctCompiler(String defaultName) {
091    name = requireNonNull(defaultName, "name");
092
093    annotationProcessors = new ArrayList<>();
094    annotationProcessorOptions = new ArrayList<>();
095    compilerOptions = new ArrayList<>();
096    showWarnings = DEFAULT_SHOW_WARNINGS;
097    showDeprecationWarnings = DEFAULT_SHOW_DEPRECATION_WARNINGS;
098    failOnWarnings = DEFAULT_FAIL_ON_WARNINGS;
099    compilationMode = DEFAULT_COMPILATION_MODE;
100    locale = DEFAULT_LOCALE;
101    logCharset = DEFAULT_LOG_CHARSET;
102    previewFeatures = DEFAULT_PREVIEW_FEATURES;
103    verbose = DEFAULT_VERBOSE;
104    diagnosticLoggingMode = DEFAULT_DIAGNOSTIC_LOGGING_MODE;
105    fixJvmModulePathMismatch = DEFAULT_FIX_JVM_MODULE_PATH_MISMATCH;
106    inheritClassPath = DEFAULT_INHERIT_CLASS_PATH;
107    inheritModulePath = DEFAULT_INHERIT_MODULE_PATH;
108    inheritSystemModulePath = DEFAULT_INHERIT_SYSTEM_MODULE_PATH;
109    fileManagerLoggingMode = DEFAULT_FILE_MANAGER_LOGGING_MODE;
110    annotationProcessorDiscovery = DEFAULT_ANNOTATION_PROCESSOR_DISCOVERY;
111    debuggingInfo = DEFAULT_DEBUGGING_INFO;
112    parameterInfoEnabled = DEFAULT_PARAMETER_INFO_ENABLED;
113
114    // If none of these are overridden then we assume the defaults instead.
115    release = null;
116    source = null;
117    target = null;
118  }
119
120  @Override
121  public JctCompilation compile(Workspace workspace) {
122    return performCompilation(workspace, null);
123  }
124
125  @Override
126  public JctCompilation compile(Workspace workspace, Collection<String> classNames) {
127    // There is no reason to invoke this overload with null values, so
128    // prevent this.
129    requireNonNullValues(classNames, "classNames");
130    return performCompilation(workspace, classNames);
131  }
132
133  @Override
134  public final <E extends Exception> AbstractJctCompiler configure(
135      JctCompilerConfigurer<E> configurer
136  ) throws E {
137    requireNonNull(configurer, "configurer");
138    configurer.configure(this);
139    return this;
140  }
141
142  @Override
143  public String getName() {
144    return name;
145  }
146
147  @Override
148  public AbstractJctCompiler name(String name) {
149    requireNonNull(name, "name");
150    this.name = name;
151    return this;
152  }
153
154  @Override
155  public boolean isVerbose() {
156    return verbose;
157  }
158
159  @Override
160  public AbstractJctCompiler verbose(boolean enabled) {
161    verbose = enabled;
162    return this;
163  }
164
165  @Override
166  public boolean isPreviewFeatures() {
167    return previewFeatures;
168  }
169
170  @Override
171  public AbstractJctCompiler previewFeatures(boolean enabled) {
172    previewFeatures = enabled;
173    return this;
174  }
175
176  @Override
177  public boolean isShowWarnings() {
178    return showWarnings;
179  }
180
181  @Override
182  public AbstractJctCompiler showWarnings(boolean enabled) {
183    showWarnings = enabled;
184    return this;
185  }
186
187  @Override
188  public boolean isShowDeprecationWarnings() {
189    return showDeprecationWarnings;
190  }
191
192  @Override
193  public AbstractJctCompiler showDeprecationWarnings(boolean enabled) {
194    showDeprecationWarnings = enabled;
195    return this;
196  }
197
198  @Override
199  public boolean isFailOnWarnings() {
200    return failOnWarnings;
201  }
202
203  @Override
204  public AbstractJctCompiler failOnWarnings(boolean enabled) {
205    failOnWarnings = enabled;
206    return this;
207  }
208
209  @Override
210  public CompilationMode getCompilationMode() {
211    return compilationMode;
212  }
213
214  @Override
215  public AbstractJctCompiler compilationMode(CompilationMode compilationMode) {
216    this.compilationMode = compilationMode;
217    return this;
218  }
219
220  @Override
221  public List<String> getAnnotationProcessorOptions() {
222    return List.copyOf(annotationProcessorOptions);
223  }
224
225  @Override
226  public AbstractJctCompiler addAnnotationProcessorOptions(
227      Iterable<String> annotationProcessorOptions
228  ) {
229    requireNonNullValues(annotationProcessorOptions, "annotationProcessorOptions");
230    annotationProcessorOptions.forEach(this.annotationProcessorOptions::add);
231    return this;
232  }
233
234  @Override
235  public List<Processor> getAnnotationProcessors() {
236    return List.copyOf(annotationProcessors);
237  }
238
239  @Override
240  public AbstractJctCompiler addAnnotationProcessors(
241      Iterable<? extends Processor> annotationProcessors
242  ) {
243    requireNonNullValues(annotationProcessors, "annotationProcessors");
244    annotationProcessors.forEach(this.annotationProcessors::add);
245
246    return this;
247  }
248
249  @Override
250  public List<String> getCompilerOptions() {
251    return List.copyOf(compilerOptions);
252  }
253
254  @Override
255  public AbstractJctCompiler addCompilerOptions(Iterable<String> compilerOptions) {
256    requireNonNullValues(compilerOptions, "compilerOptions");
257    compilerOptions.forEach(this.compilerOptions::add);
258    return this;
259  }
260
261  @Override
262  public String getEffectiveRelease() {
263    if (release != null) {
264      return release;
265    }
266
267    if (target != null) {
268      return target;
269    }
270
271    return getDefaultRelease();
272  }
273
274  @Nullable
275  @Override
276  public String getRelease() {
277    return release;
278  }
279
280  @Override
281  public AbstractJctCompiler release(@Nullable String release) {
282    this.release = release;
283
284    if (release != null) {
285      source = null;
286      target = null;
287    }
288
289    return this;
290  }
291
292  @Nullable
293  @Override
294  public String getSource() {
295    return source;
296  }
297
298  @Override
299  public AbstractJctCompiler source(@Nullable String source) {
300    this.source = source;
301    if (source != null) {
302      release = null;
303    }
304    return this;
305  }
306
307  @Nullable
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}