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}