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