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}