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