001/*
002 * Copyright (C) 2022 - 2023, 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.LoggingMode;
027import io.github.ascopes.jct.workspaces.Workspace;
028import java.io.IOException;
029import java.nio.charset.Charset;
030import java.util.ArrayList;
031import java.util.Collection;
032import java.util.List;
033import java.util.Locale;
034import javax.annotation.processing.Processor;
035import org.apiguardian.api.API;
036import org.apiguardian.api.API.Status;
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</strong> thread-safe.
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 */
056@API(since = "0.0.1", status = Status.STABLE)
057public abstract class AbstractJctCompiler implements JctCompiler {
058
059  private final List<Processor> annotationProcessors;
060  private final List<String> annotationProcessorOptions;
061  private final List<String> compilerOptions;
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 @Nullable String release;
072  private @Nullable String source;
073  private @Nullable String target;
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
082  /**
083   * Initialize this compiler.
084   *
085   * @param defaultName the printable default name to use for the compiler.
086   */
087  protected AbstractJctCompiler(String defaultName) {
088    name = requireNonNull(defaultName, "name");
089    annotationProcessors = new ArrayList<>();
090    annotationProcessorOptions = new ArrayList<>();
091    compilerOptions = new ArrayList<>();
092    showWarnings = JctCompiler.DEFAULT_SHOW_WARNINGS;
093    showDeprecationWarnings = JctCompiler.DEFAULT_SHOW_DEPRECATION_WARNINGS;
094    failOnWarnings = JctCompiler.DEFAULT_FAIL_ON_WARNINGS;
095    compilationMode = JctCompiler.DEFAULT_COMPILATION_MODE;
096    locale = JctCompiler.DEFAULT_LOCALE;
097    logCharset = JctCompiler.DEFAULT_LOG_CHARSET;
098    previewFeatures = JctCompiler.DEFAULT_PREVIEW_FEATURES;
099    release = null;
100    source = null;
101    target = null;
102    verbose = JctCompiler.DEFAULT_VERBOSE;
103    diagnosticLoggingMode = JctCompiler.DEFAULT_DIAGNOSTIC_LOGGING_MODE;
104    fixJvmModulePathMismatch = JctCompiler.DEFAULT_FIX_JVM_MODULE_PATH_MISMATCH;
105    inheritClassPath = JctCompiler.DEFAULT_INHERIT_CLASS_PATH;
106    inheritModulePath = JctCompiler.DEFAULT_INHERIT_MODULE_PATH;
107    inheritSystemModulePath = JctCompiler.DEFAULT_INHERIT_SYSTEM_MODULE_PATH;
108    fileManagerLoggingMode = JctCompiler.DEFAULT_FILE_MANAGER_LOGGING_MODE;
109    annotationProcessorDiscovery = JctCompiler.DEFAULT_ANNOTATION_PROCESSOR_DISCOVERY;
110  }
111
112  @Override
113  public JctCompilation compile(Workspace workspace) {
114    return compileInternal(workspace, null);
115  }
116
117  @Override
118  public JctCompilation compile(Workspace workspace, Collection<String> classNames) {
119    return compileInternal(workspace, classNames);
120  }
121
122  @Override
123  public final <E extends Exception> AbstractJctCompiler configure(JctCompilerConfigurer<E> configurer) throws E {
124    requireNonNull(configurer, "configurer");
125    configurer.configure(this);
126    return this;
127  }
128
129  @Override
130  public String getName() {
131    return name;
132  }
133
134  @Override
135  public AbstractJctCompiler name(String name) {
136    this.name = requireNonNull(name, "name");
137    return this;
138  }
139
140  @Override
141  public boolean isVerbose() {
142    return verbose;
143  }
144
145  @Override
146  public AbstractJctCompiler verbose(boolean enabled) {
147    verbose = enabled;
148    return this;
149  }
150
151  @Override
152  public boolean isPreviewFeatures() {
153    return previewFeatures;
154  }
155
156  @Override
157  public AbstractJctCompiler previewFeatures(boolean enabled) {
158    previewFeatures = enabled;
159    return this;
160  }
161
162  @Override
163  public boolean isShowWarnings() {
164    return showWarnings;
165  }
166
167  @Override
168  public AbstractJctCompiler showWarnings(boolean enabled) {
169    showWarnings = enabled;
170    return this;
171  }
172
173  @Override
174  public boolean isShowDeprecationWarnings() {
175    return showDeprecationWarnings;
176  }
177
178  @Override
179  public AbstractJctCompiler showDeprecationWarnings(boolean enabled) {
180    showDeprecationWarnings = enabled;
181    return this;
182  }
183
184  @Override
185  public boolean isFailOnWarnings() {
186    return failOnWarnings;
187  }
188
189  @Override
190  public AbstractJctCompiler failOnWarnings(boolean enabled) {
191    failOnWarnings = enabled;
192    return this;
193  }
194
195  @Override
196  public CompilationMode getCompilationMode() {
197    return compilationMode;
198  }
199
200  @Override
201  public AbstractJctCompiler compilationMode(CompilationMode compilationMode) {
202    this.compilationMode = compilationMode;
203    return this;
204  }
205
206  @Override
207  public List<String> getAnnotationProcessorOptions() {
208    return List.copyOf(annotationProcessorOptions);
209  }
210
211  @Override
212  public AbstractJctCompiler addAnnotationProcessorOptions(Iterable<String> annotationProcessorOptions) {
213    requireNonNullValues(annotationProcessorOptions, "annotationProcessorOptions");
214    annotationProcessorOptions.forEach(this.annotationProcessorOptions::add);
215    return this;
216  }
217
218  @Override
219  public List<Processor> getAnnotationProcessors() {
220    return List.copyOf(annotationProcessors);
221  }
222
223  @Override
224  public AbstractJctCompiler addAnnotationProcessors(Iterable<? extends Processor> annotationProcessors) {
225    requireNonNullValues(annotationProcessors, "annotationProcessors");
226    annotationProcessors.forEach(this.annotationProcessors::add);
227
228    return this;
229  }
230
231  @Override
232  public List<String> getCompilerOptions() {
233    return List.copyOf(compilerOptions);
234  }
235
236  @Override
237  public AbstractJctCompiler addCompilerOptions(Iterable<String> compilerOptions) {
238    requireNonNullValues(compilerOptions, "compilerOptions");
239    compilerOptions.forEach(this.compilerOptions::add);
240    return this;
241  }
242
243  @Override
244  public String getEffectiveRelease() {
245    if (release != null) {
246      return release;
247    }
248
249    if (target != null) {
250      return target;
251    }
252
253    return getDefaultRelease();
254  }
255
256  @Override
257  public String getRelease() {
258    return release;
259  }
260
261  @Override
262  public AbstractJctCompiler release(@Nullable String release) {
263    this.release = release;
264
265    if (release != null) {
266      source = null;
267      target = null;
268    }
269
270    return this;
271  }
272
273  @Override
274  public String getSource() {
275    return source;
276  }
277
278  @Override
279  public AbstractJctCompiler source(@Nullable String source) {
280    this.source = source;
281    if (source != null) {
282      release = null;
283    }
284    return this;
285  }
286
287  @Override
288  public String getTarget() {
289    return target;
290  }
291
292  @Override
293  public AbstractJctCompiler target(@Nullable String target) {
294    this.target = target;
295    if (target != null) {
296      release = null;
297    }
298    return this;
299  }
300
301  @Override
302  public boolean isFixJvmModulePathMismatch() {
303    return fixJvmModulePathMismatch;
304  }
305
306  @Override
307  public AbstractJctCompiler fixJvmModulePathMismatch(boolean fixJvmModulePathMismatch) {
308    this.fixJvmModulePathMismatch = fixJvmModulePathMismatch;
309    return this;
310  }
311
312  @Override
313  public boolean isInheritClassPath() {
314    return inheritClassPath;
315  }
316
317  @Override
318  public AbstractJctCompiler inheritClassPath(boolean inheritClassPath) {
319    this.inheritClassPath = inheritClassPath;
320    return this;
321  }
322
323  @Override
324  public boolean isInheritModulePath() {
325    return inheritModulePath;
326  }
327
328  @Override
329  public AbstractJctCompiler inheritModulePath(boolean inheritModulePath) {
330    this.inheritModulePath = inheritModulePath;
331    return this;
332  }
333
334  @Override
335  public boolean isInheritSystemModulePath() {
336    return inheritSystemModulePath;
337  }
338
339  @Override
340  public AbstractJctCompiler inheritSystemModulePath(boolean inheritSystemModulePath) {
341    this.inheritSystemModulePath = inheritSystemModulePath;
342    return this;
343  }
344
345  @Override
346  public Locale getLocale() {
347    return locale;
348  }
349
350  @Override
351  public AbstractJctCompiler locale(Locale locale) {
352    requireNonNull(locale, "locale");
353    this.locale = locale;
354    return this;
355  }
356
357  @Override
358  public Charset getLogCharset() {
359    return logCharset;
360  }
361
362  @Override
363  public AbstractJctCompiler logCharset(Charset logCharset) {
364    requireNonNull(logCharset, "logCharset");
365    this.logCharset = logCharset;
366    return this;
367  }
368
369  @Override
370  public LoggingMode getFileManagerLoggingMode() {
371    return fileManagerLoggingMode;
372  }
373
374  @Override
375  public AbstractJctCompiler fileManagerLoggingMode(LoggingMode fileManagerLoggingMode) {
376    this.fileManagerLoggingMode = requireNonNull(fileManagerLoggingMode, "fileManagerLoggingMode");
377    return this;
378  }
379
380  @Override
381  public LoggingMode getDiagnosticLoggingMode() {
382    return diagnosticLoggingMode;
383  }
384
385  @Override
386  public AbstractJctCompiler diagnosticLoggingMode(LoggingMode diagnosticLoggingMode) {
387    requireNonNull(diagnosticLoggingMode, "diagnosticLoggingMode");
388    this.diagnosticLoggingMode = diagnosticLoggingMode;
389    return this;
390  }
391
392  @Override
393  public AnnotationProcessorDiscovery getAnnotationProcessorDiscovery() {
394    return annotationProcessorDiscovery;
395  }
396
397  @Override
398  public AbstractJctCompiler annotationProcessorDiscovery(AnnotationProcessorDiscovery annotationProcessorDiscovery) {
399    this.annotationProcessorDiscovery = requireNonNull(
400        annotationProcessorDiscovery,
401        "annotationProcessorDiscovery"
402    );
403    return this;
404  }
405
406  /**
407   * Get the string representation of the compiler.
408   *
409   * @return the string representation of the compiler.
410   */
411  @Override
412  public final String toString() {
413    // This returns the compiler name to simplify parameterization naming in @JavacCompilerTest
414    // parameterized tests.
415    return name;
416  }
417
418  /**
419   * Get the flag builder factory to use for building flags.
420   *
421   * @return the factory.
422   */
423  public abstract JctFlagBuilderFactory getFlagBuilderFactory();
424
425  /**
426   * Get the JSR-199 compiler factory to use for initialising an internal compiler.
427   *
428   * @return the factory.
429   */
430  public abstract Jsr199CompilerFactory getCompilerFactory();
431
432  /**
433   * Get the file manager factory to use for building AbstractJctCompiler file manager during compilation.
434   *
435   * @return the factory.
436   */
437  public abstract JctFileManagerFactory getFileManagerFactory();
438
439  /**
440   * Get the compilation factory to use for building a compilation.
441   *
442   * <p>By default, this uses a common internal implementation that is designed to work with
443   * compilers that have interfaces the same as, and behave the same as Javac.
444   *
445   * <p>Some obscure compiler implementations with potentially satanic rituals for initialising
446   * and configuring components correctly may need to provide a custom implementation here instead.
447   * In this case, this method should be overridden.
448   *
449   * @return the compilation factory.
450   */
451  public JctCompilationFactory getCompilationFactory() {
452    return new JctCompilationFactoryImpl(this);
453  }
454
455  /**
456   * {@inheritDoc}
457   *
458   * @return the default release version to use when no version is specified by the user.
459   */
460  @Override
461  public abstract String getDefaultRelease();
462
463  /**
464   * Build the list of flags from this compiler object using the flag builder.
465   *
466   * <p>Implementations should not need to override this unless there is a special edge case
467   * that needs configuring differently. This is exposed to assist in these kinds of cases.
468   *
469   * @param flagBuilder the flag builder to apply the flag configuration to.
470   * @return the string flags to use.
471   */
472  protected List<String> buildFlags(JctFlagBuilder flagBuilder) {
473    return flagBuilder
474        .annotationProcessorOptions(annotationProcessorOptions)
475        .showDeprecationWarnings(showDeprecationWarnings)
476        .failOnWarnings(failOnWarnings)
477        .compilerOptions(compilerOptions)
478        .previewFeatures(previewFeatures)
479        .release(release)
480        .source(source)
481        .target(target)
482        .verbose(verbose)
483        .showWarnings(showWarnings)
484        .build();
485  }
486
487  @SuppressWarnings("ThrowFromFinallyBlock")
488  private JctCompilation compileInternal(Workspace workspace, Collection<String> classNames) {
489    var fileManagerFactory = getFileManagerFactory();
490    var flagBuilderFactory = getFlagBuilderFactory();
491    var compilerFactory = getCompilerFactory();
492    var compilationFactory = getCompilationFactory();
493    var flags = buildFlags(flagBuilderFactory.createFlagBuilder());
494    var compiler = compilerFactory.createCompiler();
495    var fileManager = fileManagerFactory.createFileManager(workspace);
496
497    // Any internal exceptions should be rethrown as a JctCompilerException by the
498    // compilation factory, so there is nothing else to worry about here.
499    // Likewise, do not catch IOException on the compilation process, as it may hide
500    // bugs. Hence, we have to use a try-finally-try-catch-rethrow manually rather than a nice
501    // try-with-resources. This is kinda crap code, but it prevents reporting errors incorrectly.
502
503    try {
504      return compilationFactory.createCompilation(flags, fileManager, compiler, classNames);
505    } finally {
506      try {
507        fileManager.close();
508      } catch (IOException ex) {
509        throw new JctCompilerException(
510            "Failed to close file manager. This is probably a bug, so please report it.",
511            ex
512        );
513      }
514    }
515  }
516}