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}