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</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  private String name;
065  private boolean showWarnings;
066  private boolean showDeprecationWarnings;
067  private boolean failOnWarnings;
068  private CompilationMode compilationMode;
069  private Locale locale;
070  private Charset logCharset;
071  private boolean verbose;
072  private boolean previewFeatures;
073  private @Nullable String release;
074  private @Nullable String source;
075  private @Nullable String target;
076  private LoggingMode diagnosticLoggingMode;
077  private boolean fixJvmModulePathMismatch;
078  private boolean inheritClassPath;
079  private boolean inheritModulePath;
080  private boolean inheritSystemModulePath;
081  private LoggingMode fileManagerLoggingMode;
082  private AnnotationProcessorDiscovery annotationProcessorDiscovery;
083  private Set<DebuggingInfo> debuggingInfo;
084  private boolean parameterInfoEnabled;
085
086  /**
087   * Initialize this compiler.
088   *
089   * @param defaultName the printable default name to use for the compiler.
090   */
091  protected AbstractJctCompiler(String defaultName) {
092    name = requireNonNull(defaultName, "name");
093    annotationProcessors = new ArrayList<>(1);
094    annotationProcessorOptions = new ArrayList<>(1);
095    compilerOptions = new ArrayList<>(3);
096    showWarnings = JctCompiler.DEFAULT_SHOW_WARNINGS;
097    showDeprecationWarnings = JctCompiler.DEFAULT_SHOW_DEPRECATION_WARNINGS;
098    failOnWarnings = JctCompiler.DEFAULT_FAIL_ON_WARNINGS;
099    compilationMode = JctCompiler.DEFAULT_COMPILATION_MODE;
100    locale = JctCompiler.DEFAULT_LOCALE;
101    logCharset = JctCompiler.DEFAULT_LOG_CHARSET;
102    previewFeatures = JctCompiler.DEFAULT_PREVIEW_FEATURES;
103    release = null;
104    source = null;
105    target = null;
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
118  @Override
119  public JctCompilation compile(Workspace workspace) {
120    return compileInternal(workspace, null);
121  }
122
123  @Override
124  public JctCompilation compile(Workspace workspace, Collection<String> classNames) {
125    // There is no reason to invoke this overload with null values, so
126    // prevent this.
127    requireNonNullValues(classNames, "classNames");
128
129    if (classNames.isEmpty()) {
130      throw new IllegalArgumentException("classNames must not be empty");
131    }
132
133    return compileInternal(workspace, classNames);
134  }
135
136  @Override
137  public final <E extends Exception> AbstractJctCompiler configure(
138      JctCompilerConfigurer<E> configurer) throws E {
139    requireNonNull(configurer, "configurer");
140    configurer.configure(this);
141    return this;
142  }
143
144  @Override
145  public String getName() {
146    return name;
147  }
148
149  @Override
150  public AbstractJctCompiler name(String name) {
151    requireNonNull(name, "name");
152    this.name = name;
153    return this;
154  }
155
156  @Override
157  public boolean isVerbose() {
158    return verbose;
159  }
160
161  @Override
162  public AbstractJctCompiler verbose(boolean enabled) {
163    verbose = enabled;
164    return this;
165  }
166
167  @Override
168  public boolean isPreviewFeatures() {
169    return previewFeatures;
170  }
171
172  @Override
173  public AbstractJctCompiler previewFeatures(boolean enabled) {
174    previewFeatures = enabled;
175    return this;
176  }
177
178  @Override
179  public boolean isShowWarnings() {
180    return showWarnings;
181  }
182
183  @Override
184  public AbstractJctCompiler showWarnings(boolean enabled) {
185    showWarnings = enabled;
186    return this;
187  }
188
189  @Override
190  public boolean isShowDeprecationWarnings() {
191    return showDeprecationWarnings;
192  }
193
194  @Override
195  public AbstractJctCompiler showDeprecationWarnings(boolean enabled) {
196    showDeprecationWarnings = enabled;
197    return this;
198  }
199
200  @Override
201  public boolean isFailOnWarnings() {
202    return failOnWarnings;
203  }
204
205  @Override
206  public AbstractJctCompiler failOnWarnings(boolean enabled) {
207    failOnWarnings = enabled;
208    return this;
209  }
210
211  @Override
212  public CompilationMode getCompilationMode() {
213    return compilationMode;
214  }
215
216  @Override
217  public AbstractJctCompiler compilationMode(CompilationMode compilationMode) {
218    this.compilationMode = compilationMode;
219    return this;
220  }
221
222  @Override
223  public List<String> getAnnotationProcessorOptions() {
224    return List.copyOf(annotationProcessorOptions);
225  }
226
227  @Override
228  public AbstractJctCompiler addAnnotationProcessorOptions(
229      Iterable<String> annotationProcessorOptions) {
230    requireNonNullValues(annotationProcessorOptions, "annotationProcessorOptions");
231    annotationProcessorOptions.forEach(this.annotationProcessorOptions::add);
232    return this;
233  }
234
235  @Override
236  public List<Processor> getAnnotationProcessors() {
237    return List.copyOf(annotationProcessors);
238  }
239
240  @Override
241  public AbstractJctCompiler addAnnotationProcessors(
242      Iterable<? extends Processor> annotationProcessors) {
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  @Override
275  public String getRelease() {
276    return release;
277  }
278
279  @Override
280  public AbstractJctCompiler release(@Nullable String release) {
281    this.release = release;
282
283    if (release != null) {
284      source = null;
285      target = null;
286    }
287
288    return this;
289  }
290
291  @Override
292  public String getSource() {
293    return source;
294  }
295
296  @Override
297  public AbstractJctCompiler source(@Nullable String source) {
298    this.source = source;
299    if (source != null) {
300      release = null;
301    }
302    return this;
303  }
304
305  @Override
306  public String getTarget() {
307    return target;
308  }
309
310  @Override
311  public AbstractJctCompiler target(@Nullable String target) {
312    this.target = target;
313    if (target != null) {
314      release = null;
315    }
316    return this;
317  }
318
319  @Override
320  public boolean isFixJvmModulePathMismatch() {
321    return fixJvmModulePathMismatch;
322  }
323
324  @Override
325  public AbstractJctCompiler fixJvmModulePathMismatch(boolean fixJvmModulePathMismatch) {
326    this.fixJvmModulePathMismatch = fixJvmModulePathMismatch;
327    return this;
328  }
329
330  @Override
331  public boolean isInheritClassPath() {
332    return inheritClassPath;
333  }
334
335  @Override
336  public AbstractJctCompiler inheritClassPath(boolean inheritClassPath) {
337    this.inheritClassPath = inheritClassPath;
338    return this;
339  }
340
341  @Override
342  public boolean isInheritModulePath() {
343    return inheritModulePath;
344  }
345
346  @Override
347  public AbstractJctCompiler inheritModulePath(boolean inheritModulePath) {
348    this.inheritModulePath = inheritModulePath;
349    return this;
350  }
351
352  @Override
353  public boolean isInheritSystemModulePath() {
354    return inheritSystemModulePath;
355  }
356
357  @Override
358  public AbstractJctCompiler inheritSystemModulePath(boolean inheritSystemModulePath) {
359    this.inheritSystemModulePath = inheritSystemModulePath;
360    return this;
361  }
362
363  @Override
364  public Locale getLocale() {
365    return locale;
366  }
367
368  @Override
369  public AbstractJctCompiler locale(Locale locale) {
370    requireNonNull(locale, "locale");
371    this.locale = locale;
372    return this;
373  }
374
375  @Override
376  public Charset getLogCharset() {
377    return logCharset;
378  }
379
380  @Override
381  public AbstractJctCompiler logCharset(Charset logCharset) {
382    requireNonNull(logCharset, "logCharset");
383    this.logCharset = logCharset;
384    return this;
385  }
386
387  @Override
388  public LoggingMode getFileManagerLoggingMode() {
389    return fileManagerLoggingMode;
390  }
391
392  @Override
393  public AbstractJctCompiler fileManagerLoggingMode(LoggingMode fileManagerLoggingMode) {
394    requireNonNull(fileManagerLoggingMode, "fileManagerLoggingMode");
395    this.fileManagerLoggingMode = fileManagerLoggingMode;
396    return this;
397  }
398
399  @Override
400  public LoggingMode getDiagnosticLoggingMode() {
401    return diagnosticLoggingMode;
402  }
403
404  @Override
405  public AbstractJctCompiler diagnosticLoggingMode(LoggingMode diagnosticLoggingMode) {
406    requireNonNull(diagnosticLoggingMode, "diagnosticLoggingMode");
407    this.diagnosticLoggingMode = diagnosticLoggingMode;
408    return this;
409  }
410
411  @Override
412  public AnnotationProcessorDiscovery getAnnotationProcessorDiscovery() {
413    return annotationProcessorDiscovery;
414  }
415
416  @Override
417  public AbstractJctCompiler annotationProcessorDiscovery(
418      AnnotationProcessorDiscovery annotationProcessorDiscovery) {
419    requireNonNull(annotationProcessorDiscovery, 
420        "annotationProcessorDiscovery");
421    this.annotationProcessorDiscovery = annotationProcessorDiscovery;
422    return this;
423  }
424
425  @Override
426  public Set<DebuggingInfo> getDebuggingInfo() {
427    return debuggingInfo;
428  }
429
430  @Override
431  public JctCompiler debuggingInfo(Set<DebuggingInfo> debuggingInfo) {
432    requireNonNullValues(debuggingInfo, "debuggingInfo");
433    this.debuggingInfo = Set.copyOf(debuggingInfo);
434    return this;
435  }
436
437  @Override
438  public boolean isParameterInfoEnabled() {
439    return parameterInfoEnabled;
440  }
441
442  @Override
443  public JctCompiler parameterInfoEnabled(boolean parameterInfoEnabled) {
444    this.parameterInfoEnabled = parameterInfoEnabled;
445    return this;
446  }
447
448  /**
449   * Get the string representation of the compiler.
450   *
451   * @return the string representation of the compiler.
452   */
453  @Override
454  public final String toString() {
455    // This returns the compiler name to simplify parameterization naming in @JavacCompilerTest
456    // parameterized tests.
457    return name;
458  }
459
460  /**
461   * Get the flag builder factory to use for building flags.
462   *
463   * @return the factory.
464   */
465  public abstract JctFlagBuilderFactory getFlagBuilderFactory();
466
467  /**
468   * Get the JSR-199 compiler factory to use for initialising an internal compiler.
469   *
470   * @return the factory.
471   */
472  public abstract Jsr199CompilerFactory getCompilerFactory();
473
474  /**
475   * Get the file manager factory to use for building AbstractJctCompiler file manager during
476   * compilation.
477   *
478   * <p>Since v1.1.0, this method has provided a default implementation. Before this, it was
479   * abstract. The default implementation calls
480   * {@link JctFileManagers#newJctFileManagerFactory(JctCompiler)}.
481   *
482   * @return the factory.
483   */
484  public JctFileManagerFactory getFileManagerFactory() {
485    return JctFileManagers.newJctFileManagerFactory(this);
486  }
487
488  /**
489   * Get the compilation factory to use for building a compilation.
490   *
491   * <p>By default, this uses a common internal implementation that is designed to work with
492   * compilers that have interfaces the same as, and behave the same as Javac.
493   *
494   * <p>Some obscure compiler implementations with potentially satanic rituals for initialising
495   * and configuring components correctly may need to provide a custom implementation here instead.
496   * In this case, this method should be overridden. Base classes are not provided for you to extend
497   * in this case as this is usually not something you want to be doing. Instead, you should
498   * implement {@link JctCompilationFactory} directly.
499   *
500   * @return the compilation factory.
501   */
502  public JctCompilationFactory getCompilationFactory() {
503    return new JctCompilationFactoryImpl(this);
504  }
505
506  /**
507   * {@inheritDoc}
508   *
509   * @return the default release version to use when no version is specified by the user.
510   */
511  @Override
512  public abstract String getDefaultRelease();
513
514  /**
515   * Build the list of flags from this compiler object using the flag builder.
516   *
517   * <p>Implementations should not need to override this unless there is a special edge case
518   * that needs configuring differently. This is exposed to assist in these kinds of cases.
519   *
520   * @param flagBuilder the flag builder to apply the flag configuration to.
521   * @return the string flags to use.
522   */
523  protected List<String> buildFlags(JctFlagBuilder flagBuilder) {
524    return flagBuilder
525        .annotationProcessorOptions(annotationProcessorOptions)
526        .showDeprecationWarnings(showDeprecationWarnings)
527        .failOnWarnings(failOnWarnings)
528        .compilerOptions(compilerOptions)
529        .previewFeatures(previewFeatures)
530        .release(release)
531        .source(source)
532        .target(target)
533        .verbose(verbose)
534        .showWarnings(showWarnings)
535        .debuggingInfo(debuggingInfo)
536        .parameterInfoEnabled(parameterInfoEnabled)
537        .build();
538  }
539
540  @SuppressWarnings("ThrowFromFinallyBlock")
541  private JctCompilation compileInternal(
542        Workspace workspace, 
543        @Nullable Collection<String> classNames
544  ) {
545    var fileManagerFactory = getFileManagerFactory();
546    var flagBuilderFactory = getFlagBuilderFactory();
547    var compilerFactory = getCompilerFactory();
548    var compilationFactory = getCompilationFactory();
549    var flags = buildFlags(flagBuilderFactory.createFlagBuilder());
550    var compiler = compilerFactory.createCompiler();
551    var fileManager = fileManagerFactory.createFileManager(workspace);
552
553    // Any internal exceptions should be rethrown as a JctCompilerException by the
554    // compilation factory, so there is nothing else to worry about here.
555    // Likewise, do not catch IOException on the compilation process, as it may hide
556    // bugs. Hence, we have to use a try-finally-try-catch-rethrow manually rather than a nice
557    // try-with-resources. This is kinda crap code, but it prevents reporting errors incorrectly.
558
559    try {
560      return compilationFactory.createCompilation(flags, fileManager, compiler, classNames);
561    } finally {
562      try {
563        fileManager.close();
564      } catch (IOException ex) {
565        throw new JctCompilerException(
566            "Failed to close file manager. This is probably a bug, so please report it.",
567            ex
568        );
569      }
570    }
571  }
572}