diff --git a/README.md b/README.md index 00d55078..e1b09426 100644 --- a/README.md +++ b/README.md @@ -306,6 +306,24 @@ Or in Maven: 📖 **For complete documentation, examples, and all available options, see the [Configuration Guide](docs/CONFIGURATION.md).** +#### Strict mode (fail the build on generation errors) + +By default, if generation fails (for a builder or for a Jackson module) the processor emits a **compiler warning** and continues, so the build still succeeds. This keeps local/iterative development smooth. + +For high-reliability builds (e.g. CI/release pipelines) you can opt in to **strict mode**, which promotes generation failures — including Jackson module generation failures — to **compiler errors** that fail the build: + +```bash +javac -Asimplebuilder.strict=true YourClass.java +``` + +```xml + + -Asimplebuilder.strict=true + +``` + +Strict mode is **disabled by default** (`false`); the default behavior (warnings only, build does not fail) is unchanged. + ## Examples The `example` module contains real-world examples demonstrating various builder configurations and features. You can explore the source DTOs and their generated builders: diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java index 07530341..0373d763 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java @@ -67,6 +67,13 @@ public class BuilderProcessor extends AbstractProcessor { private JacksonModuleGenerator jacksonModuleGenerator; private boolean supportedJdk = true; + /** + * Opt-in strict/fail-fast mode. When enabled (via {@code -Asimplebuilder.strict=true}), + * generation failures are reported as compiler errors that fail the build. Defaults to {@code + * false} (warnings only, build does not fail). + */ + private boolean strict = false; + @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); @@ -78,6 +85,9 @@ public synchronized void init(ProcessingEnvironment processingEnv) { BuilderConfiguration globalConfig = reader.readBuilderConfiguration(); logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig); + this.strict = reader.readBooleanValue(CompilerArgumentsEnum.STRICT); + logger.debug("Strict generation mode: %s", this.strict); + this.context = new ProcessingContext(logger, globalConfig, processingEnv); this.codeGenerator = new RoasterCodeGenerator(processingEnv, logger); this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger); @@ -116,9 +126,17 @@ public boolean process(Set annotations, RoundEnvironment try { codeGenerator.generateClass(moduleClassDef); } catch (BuilderException e) { - context.warning( - "simple-builders: Error generating Jackson module for package %s: %s", - packageName, e.getMessage()); + // By default Jackson module generation failures are warnings. In opt-in strict mode + // they are promoted to errors that fail the build. + if (strict) { + context.error( + "simple-builders: Error generating Jackson module for package %s: %s", + packageName, e.getMessage()); + } else { + context.warning( + "simple-builders: Error generating Jackson module for package %s: %s", + packageName, e.getMessage()); + } } } // Reset indentation after Jackson module generation as well diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java index 01e0ff45..cf40d714 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java @@ -131,7 +131,15 @@ public enum CompilerArgumentsEnum { // === Logging === /** Option for verbose logging output. */ - VERBOSE("verbose"); + VERBOSE("verbose"), + + // === Error Handling === + /** + * Option for strict/fail-fast generation mode. When enabled, builder (and Jackson module) + * generation failures are reported as compiler errors that fail the build instead of warnings. + * Defaults to disabled (warnings only, build does not fail). + */ + STRICT("strict"); /** Compiler option prefix for all simple-builders options. */ private static final String OPTION_PREFIX = "simplebuilder."; diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleStrictModeTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleStrictModeTest.java new file mode 100644 index 00000000..439a225d --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleStrictModeTest.java @@ -0,0 +1,93 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.createCompiler; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.simpleBuilderClass; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.jupiter.api.Test; + +/** + * Tests that the opt-in strict/fail-fast mode ({@code -Asimplebuilder.strict=true}) also governs + * Jackson module generation failures. + * + *

A failure is induced by providing a hand-written class named {@code + * SimpleBuildersJacksonModule} in the DTO's package, which collides with the module the processor + * generates at the end of processing and makes generation fail with a "already exists" error. + */ +class JacksonModuleStrictModeTest { + + private static final String PACKAGE = "pkg.jacksonstrict"; + + private JavaFileObject dto() { + return simpleBuilderClass( + PACKAGE, + "JacksonStrictDto", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + } + + /** A hand-written class colliding with the generated Jackson module for the package. */ + private JavaFileObject collidingModule() { + return JavaFileObjects.forSourceLines( + PACKAGE + ".SimpleBuildersJacksonModule", + "package " + PACKAGE + ";", + "public class SimpleBuildersJacksonModule {}"); + } + + @Test + void defaultMode_jacksonModuleFailure_isWarning_andCompilationSucceeds() { + Compilation compilation = + createCompiler() + .withOptions( + "-Asimplebuilder.generateJacksonModule=true", + "-Asimplebuilder.usingJacksonDeserializerAnnotation=true") + .compile(dto(), collidingModule()); + + assertThat(compilation).succeeded(); + assertThat(compilation).hadWarningContaining("Error generating Jackson module"); + } + + @Test + void strictMode_jacksonModuleFailure_isError_andCompilationFails() { + Compilation compilation = + createCompiler() + .withOptions( + "-Asimplebuilder.generateJacksonModule=true", + "-Asimplebuilder.usingJacksonDeserializerAnnotation=true", + "-Asimplebuilder.strict=true") + .compile(dto(), collidingModule()); + + assertThat(compilation).failed(); + assertThat(compilation).hadErrorContaining("Error generating Jackson module"); + } +}