From a620a0ecae01595056a1e14506001fb7e532043e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 08:51:16 +0000 Subject: [PATCH 1/6] Add opt-in strict mode for builder-generation failures Introduce a new annotation-processor option (-Asimplebuilder.strict=true) that promotes builder-generation failures from warnings to compiler errors, failing the build. The default is unchanged: failures remain warnings and the build succeeds. Adds the STRICT compiler argument, an element-aware error reporter, wires the flag into BuilderProcessor, documents it in README, and adds tests covering both default (warning) and strict (error) behavior. Fixes #172 Co-Authored-By: Andreas Igel --- README.md | 18 ++++ .../builders/processor/BuilderProcessor.java | 27 +++++- .../processing/CompilerArgumentsEnum.java | 10 ++- .../processing/ProcessingContext.java | 11 +++ .../processing/ProcessingLogger.java | 11 +++ .../builders/processor/StrictModeTest.java | 84 +++++++++++++++++++ 6 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/StrictModeTest.java diff --git a/README.md b/README.md index 00d55078..0a30f9f0 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 a builder cannot be generated for an annotated type 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 builder-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..65410f97 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}), builder + * 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); @@ -169,10 +179,19 @@ public boolean process(Set annotations, RoundEnvironment process(annotatedElement, config); successfulGenerations++; } catch (BuilderException ex) { - // All builder generation failures are warnings to allow other builders to be - // generated - context.warning( - annotatedElement, "simple-builders: Failed to generate builder - %s", ex.getMessage()); + // By default builder generation failures are warnings so other builders are still + // generated. In opt-in strict mode they are promoted to errors that fail the build. + if (strict) { + context.error( + annotatedElement, + "simple-builders: Failed to generate builder - %s", + ex.getMessage()); + } else { + context.warning( + annotatedElement, + "simple-builders: Failed to generate builder - %s", + ex.getMessage()); + } } finally { context.debugEndOperation(); } 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/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java index f0fe7341..ae18c5f1 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java @@ -323,4 +323,15 @@ public void warning(Element element, String format, Object... args) { public void error(String format, Object... args) { logger.error(format, args); } + + /** + * Reports an error at the location of the given element with a formatted message. + * + * @param element the element where the error occurred, used for location information + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string + */ + public void error(Element element, String format, Object... args) { + logger.error(element, format, args); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingLogger.java index 6a11443e..5be6032e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingLogger.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingLogger.java @@ -75,6 +75,17 @@ public void error(String format, Object... args) { messager.printMessage(Diagnostic.Kind.ERROR, String.format(format, args)); } + /** + * Reports an error at the location of the given element with a formatted message. + * + * @param e the element where the error occurred, used for location information + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string + */ + public void error(Element e, String format, Object... args) { + messager.printMessage(Diagnostic.Kind.ERROR, String.format(format, args), e); + } + /** * Posts an info-level message (NOTE level in Maven). Used for important status information about * builder generation. diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/StrictModeTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/StrictModeTest.java new file mode 100644 index 00000000..f77c48c7 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/StrictModeTest.java @@ -0,0 +1,84 @@ +/* + * 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 for the opt-in strict/fail-fast generation mode ({@code -Asimplebuilder.strict=true}). + * + *

A builder-generation failure is induced by providing a hand-written builder class with the + * same name the processor would generate, which makes generation fail with a "Builder class already + * exists" error. This exercises the failure path in a deterministic way. + */ +class StrictModeTest { + + private static final String PACKAGE = "pkg.strict"; + + private JavaFileObject dto() { + return simpleBuilderClass( + PACKAGE, + "StrictDto", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + } + + /** A hand-written builder colliding with the generated {@code StrictDtoBuilder}. */ + private JavaFileObject collidingBuilder() { + return JavaFileObjects.forSourceLines( + PACKAGE + ".StrictDtoBuilder", + "package " + PACKAGE + ";", + "public class StrictDtoBuilder {}"); + } + + @Test + void defaultMode_generationFailure_isWarning_andCompilationSucceeds() { + Compilation compilation = createCompiler().compile(dto(), collidingBuilder()); + + assertThat(compilation).succeeded(); + assertThat(compilation).hadWarningContaining("Failed to generate builder"); + } + + @Test + void strictMode_generationFailure_isError_andCompilationFails() { + Compilation compilation = + createCompiler() + .withOptions("-Asimplebuilder.strict=true") + .compile(dto(), collidingBuilder()); + + assertThat(compilation).failed(); + assertThat(compilation).hadErrorContaining("Failed to generate builder"); + } +} From 33a1a9c70c36fd1f0b6dca55c29f85c2951684ba Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:31:52 +0000 Subject: [PATCH 2/6] Also apply strict mode to Jackson module generation failures Fold the Jackson-module strict handling (issue #173) into the shared strict option so both builder and Jackson module generation failures are promoted to compiler errors under -Asimplebuilder.strict=true, default unchanged (warnings only). Adds JacksonModuleStrictModeTest and updates the README wording. Fixes #173 Co-Authored-By: Andreas Igel --- README.md | 4 +- .../builders/processor/BuilderProcessor.java | 14 ++- .../JacksonModuleStrictModeTest.java | 93 +++++++++++++++++++ 3 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleStrictModeTest.java diff --git a/README.md b/README.md index 0a30f9f0..e1b09426 100644 --- a/README.md +++ b/README.md @@ -308,9 +308,9 @@ Or in Maven: #### Strict mode (fail the build on generation errors) -By default, if a builder cannot be generated for an annotated type the processor emits a **compiler warning** and continues, so the build still succeeds. This keeps local/iterative development smooth. +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 builder-generation failures to **compiler errors** that fail the build: +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 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 65410f97..82912920 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 @@ -126,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/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"); + } +} From 07419df12c53ba1bb110ac64f5a826659881f38e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:58:14 +0000 Subject: [PATCH 3/6] refactor: integrate strict mode into BuilderConfiguration and add helper Co-Authored-By: Andreas Igel --- .../builders/processor/BuilderProcessor.java | 36 +++------------- .../model/core/BuilderConfiguration.java | 27 +++++++++++- .../processing/CompilerArgumentsReader.java | 1 + .../processing/ProcessingContext.java | 42 +++++++++++++++++++ 4 files changed, 74 insertions(+), 32 deletions(-) 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 82912920..08be8175 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,13 +67,6 @@ 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}), builder - * 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); @@ -84,9 +77,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) { CompilerArgumentsReader reader = new CompilerArgumentsReader(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); + logger.debug("Strict generation mode: %s", globalConfig.isStrictModeEnabled()); this.context = new ProcessingContext(logger, globalConfig, processingEnv); this.codeGenerator = new RoasterCodeGenerator(processingEnv, logger); @@ -128,15 +119,9 @@ public boolean process(Set annotations, RoundEnvironment } catch (BuilderException e) { // 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()); - } + context.reportErrorOrWarning( + "simple-builders: Error generating Jackson module for package %s: %s", + packageName, e.getMessage()); } } // Reset indentation after Jackson module generation as well @@ -189,17 +174,8 @@ public boolean process(Set annotations, RoundEnvironment } catch (BuilderException ex) { // By default builder generation failures are warnings so other builders are still // generated. In opt-in strict mode they are promoted to errors that fail the build. - if (strict) { - context.error( - annotatedElement, - "simple-builders: Failed to generate builder - %s", - ex.getMessage()); - } else { - context.warning( - annotatedElement, - "simple-builders: Failed to generate builder - %s", - ex.getMessage()); - } + context.reportErrorOrWarning( + annotatedElement, "simple-builders: Failed to generate builder - %s", ex.getMessage()); } finally { context.debugEndOperation(); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderConfiguration.java index 75ce1129..b61c60d3 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderConfiguration.java @@ -64,6 +64,7 @@ * @param generateWithInterface Generate With interface * @param builderSuffix Suffix for builder class name * @param setterSuffix Suffix for setter method names + * @param strict Strict/fail-fast generation mode */ public record BuilderConfiguration( OptionState generateFieldSupplier, @@ -91,7 +92,8 @@ public record BuilderConfiguration( OptionState generateJacksonModule, String jacksonModulePackage, String builderSuffix, - String setterSuffix) { + String setterSuffix, + OptionState strict) { public static final BuilderConfiguration DEFAULT = builder() @@ -121,6 +123,7 @@ public record BuilderConfiguration( .jacksonModulePackage(null) .builderSuffix("Builder") .setterSuffix("") + .strict(DISABLED) .build(); // === Convenience accessors with 'is' prefix for boolean properties === @@ -229,6 +232,10 @@ public String getSetterSuffix() { return setterSuffix; } + public boolean isStrictModeEnabled() { + return strict == ENABLED; + } + /** * Merges this configuration with another configuration. * @@ -295,6 +302,7 @@ public BuilderConfiguration merge(BuilderConfiguration other) { .jacksonModulePackage(mergeString(other.jacksonModulePackage, this.jacksonModulePackage)) .builderSuffix(mergeString(other.builderSuffix, this.builderSuffix)) .setterSuffix(mergeString(other.setterSuffix, this.setterSuffix)) + .strict(mergeOptionState(other.strict, this.strict)) .build(); } @@ -360,6 +368,7 @@ public String toString() { .appendIfNotEmpty("jacksonModulePackage", jacksonModulePackage) .appendIfNotEmpty("builderSuffix", builderSuffix) .appendIfNotEmpty("setterSuffix", setterSuffix) + .appendValueIfSet("strict", strict) .toString(); } @@ -440,6 +449,9 @@ public static class Builder { private String builderSuffix = null; private String setterSuffix = null; + // === Error Handling === + private OptionState strict = OptionState.UNSET; + // === Setters === public Builder generateSupplier(OptionState value) { this.generateFieldSupplier = value; @@ -686,6 +698,16 @@ public Builder setterSuffix(String value) { return this; } + public Builder strict(OptionState value) { + this.strict = value; + return this; + } + + public Builder strict(boolean value) { + this.strict = value ? ENABLED : DISABLED; + return this; + } + public BuilderConfiguration build() { return new BuilderConfiguration( generateFieldSupplier, @@ -713,7 +735,8 @@ public BuilderConfiguration build() { generateJacksonModule, jacksonModulePackage, builderSuffix, - setterSuffix); + setterSuffix, + strict); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java index 43588f30..884c2820 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java @@ -174,6 +174,7 @@ public BuilderConfiguration readBuilderConfiguration() { .jacksonModulePackage(readValue(CompilerArgumentsEnum.JACKSON_MODULE_PACKAGE)) .builderSuffix(readValue(CompilerArgumentsEnum.BUILDER_SUFFIX)) .setterSuffix(readValue(CompilerArgumentsEnum.SETTER_SUFFIX)) + .strict(readOptionState(CompilerArgumentsEnum.STRICT)) .build(); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java index ae18c5f1..6af221b4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java @@ -46,6 +46,7 @@ public final class ProcessingContext { private final Elements elementUtils; private final Types typeUtils; private final ProcessingLogger logger; + private final BuilderConfiguration globalConfiguration; private final BuilderConfigurationReader configurationReader; private final ProcessingEnvironment processingEnv; private GeneratorRegistry generatorRegistry; @@ -65,6 +66,7 @@ public ProcessingContext( this.elementUtils = processingEnv.getElementUtils(); this.typeUtils = processingEnv.getTypeUtils(); this.logger = logger; + this.globalConfiguration = globalConfiguration; this.processingEnv = processingEnv; this.configurationReader = new BuilderConfigurationReader(globalConfiguration, logger, elementUtils); @@ -334,4 +336,44 @@ public void error(String format, Object... args) { public void error(Element element, String format, Object... args) { logger.error(element, format, args); } + + /** + * Returns whether strict/fail-fast generation mode is enabled via the global compiler + * configuration. + * + * @return true if {@code -Asimplebuilder.strict=true} was supplied, false otherwise + */ + public boolean isStrictModeEnabled() { + return globalConfiguration.isStrictModeEnabled(); + } + + /** + * Reports either an error or a warning depending on strict mode. In strict mode a build failure + * is emitted; otherwise a warning is logged so generation of remaining builders can continue. + * + * @param element the element associated with the problem, for compiler location information + * @param format the format string + * @param args arguments referenced by the format specifiers + */ + public void reportErrorOrWarning(Element element, String format, Object... args) { + if (isStrictModeEnabled()) { + logger.error(element, format, args); + } else { + logger.warning(element, format, args); + } + } + + /** + * Reports either an error or a warning depending on strict mode, without an element location. + * + * @param format the format string + * @param args arguments referenced by the format specifiers + */ + public void reportErrorOrWarning(String format, Object... args) { + if (isStrictModeEnabled()) { + logger.error(format, args); + } else { + logger.warning(format, args); + } + } } From fbd24248bb81588babbca74c6a1dc122adf2652a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:35:07 +0000 Subject: [PATCH 4/6] Address review: rename helper, drop unused error overload, document strict mode, merge Jackson strict tests Co-Authored-By: Andreas Igel --- docs/CONFIGURATION.md | 14 +++ .../builders/processor/BuilderProcessor.java | 5 +- .../processing/ProcessingContext.java | 21 +---- .../JacksonModuleStrictModeTest.java | 93 ------------------- .../processor/JacksonModuleWarningTest.java | 73 +++++++++++++++ 5 files changed, 94 insertions(+), 112 deletions(-) delete mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleStrictModeTest.java diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 033c4621..7534ad7b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -18,6 +18,7 @@ Simple-builders supports fine-grained configuration through the `@SimpleBuilder. - [Collection Helpers](#collection-helpers) - [Component Filtering](#component-filtering) - [Integration](#integration) + - [Reliability](#reliability) - [Examples](#examples) - [Minimal Builder](#minimal-builder) - [Internal API Builder](#internal-api-builder) @@ -846,6 +847,16 @@ public class PersonDto { // Generated method: withName(String name) instead of name(String name) ``` +### Reliability + +#### `strict` + +**Default**: `DISABLED` | **Compiler Option**: `-Asimplebuilder.strict=ENABLED|DISABLED` + +When enabled, builder and Jackson-module generation failures are promoted from compiler warnings +to errors that fail the build. By default, strict mode is disabled and generation failures are +reported as warnings so compilation can continue. + ## Examples ### Minimal Builder @@ -1231,6 +1242,9 @@ methodAccess = AccessModifier.PRIVATE # Naming -Asimplebuilder.builderSuffix=CustomSuffix -Asimplebuilder.setterSuffix=customPrefix + +# Reliability +-Asimplebuilder.strict=ENABLED|DISABLED ``` ### Complete Options Example 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 08be8175..75bda27d 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 @@ -77,7 +77,6 @@ public synchronized void init(ProcessingEnvironment processingEnv) { CompilerArgumentsReader reader = new CompilerArgumentsReader(processingEnv); BuilderConfiguration globalConfig = reader.readBuilderConfiguration(); logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig); - logger.debug("Strict generation mode: %s", globalConfig.isStrictModeEnabled()); this.context = new ProcessingContext(logger, globalConfig, processingEnv); this.codeGenerator = new RoasterCodeGenerator(processingEnv, logger); @@ -119,7 +118,7 @@ public boolean process(Set annotations, RoundEnvironment } catch (BuilderException e) { // By default Jackson module generation failures are warnings. In opt-in strict mode // they are promoted to errors that fail the build. - context.reportErrorOrWarning( + context.reportBasedOnStrictMode( "simple-builders: Error generating Jackson module for package %s: %s", packageName, e.getMessage()); } @@ -174,7 +173,7 @@ public boolean process(Set annotations, RoundEnvironment } catch (BuilderException ex) { // By default builder generation failures are warnings so other builders are still // generated. In opt-in strict mode they are promoted to errors that fail the build. - context.reportErrorOrWarning( + context.reportBasedOnStrictMode( annotatedElement, "simple-builders: Failed to generate builder - %s", ex.getMessage()); } finally { context.debugEndOperation(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java index 6af221b4..e66e4b4a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java @@ -326,17 +326,6 @@ public void error(String format, Object... args) { logger.error(format, args); } - /** - * Reports an error at the location of the given element with a formatted message. - * - * @param element the element where the error occurred, used for location information - * @param format the format string - * @param args arguments referenced by the format specifiers in the format string - */ - public void error(Element element, String format, Object... args) { - logger.error(element, format, args); - } - /** * Returns whether strict/fail-fast generation mode is enabled via the global compiler * configuration. @@ -348,14 +337,14 @@ public boolean isStrictModeEnabled() { } /** - * Reports either an error or a warning depending on strict mode. In strict mode a build failure - * is emitted; otherwise a warning is logged so generation of remaining builders can continue. + * Reports an error or warning based on strict mode. In strict mode a build failure is emitted; + * otherwise a warning is logged so generation of remaining builders can continue. * * @param element the element associated with the problem, for compiler location information * @param format the format string * @param args arguments referenced by the format specifiers */ - public void reportErrorOrWarning(Element element, String format, Object... args) { + public void reportBasedOnStrictMode(Element element, String format, Object... args) { if (isStrictModeEnabled()) { logger.error(element, format, args); } else { @@ -364,12 +353,12 @@ public void reportErrorOrWarning(Element element, String format, Object... args) } /** - * Reports either an error or a warning depending on strict mode, without an element location. + * Reports an error or warning based on strict mode, without an element location. * * @param format the format string * @param args arguments referenced by the format specifiers */ - public void reportErrorOrWarning(String format, Object... args) { + public void reportBasedOnStrictMode(String format, Object... args) { if (isStrictModeEnabled()) { logger.error(format, args); } else { 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 deleted file mode 100644 index 439a225d..00000000 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleStrictModeTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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"); - } -} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java index 7f871006..34169a0c 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java @@ -1,3 +1,27 @@ +/* + * 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; @@ -5,12 +29,15 @@ 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.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; import org.junit.jupiter.api.Test; class JacksonModuleWarningTest { + private static final String STRICT_MODE_PACKAGE = "pkg.jacksonstrict"; + @Test void generateJacksonModuleEnabled_ButAnnotationDisabled_ShouldEmitWarningAndSkipGeneration() { // Given @@ -72,4 +99,50 @@ void generateJacksonModule_WhenEnabledAndDeserializerEnabled_ShouldGenerate() { // Verify file IS generated ProcessorTestUtils.loadGeneratedSource(compilation, "SimpleBuildersJacksonModule"); } + + private JavaFileObject dto() { + return simpleBuilderClass( + STRICT_MODE_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( + STRICT_MODE_PACKAGE + ".SimpleBuildersJacksonModule", + "package " + STRICT_MODE_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"); + } } From 7aad69fdf1640d2cab913642e0e4eb7d922474b0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:12:43 +0000 Subject: [PATCH 5/6] Remove duplicate strict-mode docs from README; keep them in CONFIGURATION.md (#194) Co-Authored-By: Andreas Igel --- README.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/README.md b/README.md index 1d62a9b9..a500be14 100644 --- a/README.md +++ b/README.md @@ -306,24 +306,6 @@ 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: From 585af516e1b1a74a8304b2ce7d23e713abd02cf1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:18:17 +0000 Subject: [PATCH 6/6] Drop redundant globalConfiguration field from ProcessingContext; source strict via configurationReader (#194) Co-Authored-By: Andreas Igel --- .../processor/processing/BuilderConfigurationReader.java | 9 +++++++++ .../builders/processor/processing/ProcessingContext.java | 4 +--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java index 9a1b30f7..74f96044 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java @@ -79,6 +79,15 @@ public BuilderConfigurationReader( this.elementUtils = elementUtils; } + /** + * Gets the global builder configuration read from compiler arguments. + * + * @return the global builder configuration + */ + public BuilderConfiguration getGlobalConfiguration() { + return globalConfiguration; + } + /** * Reads builder configuration from {@code @SimpleBuilder(options = ...)} inline options. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java index e66e4b4a..2886227f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java @@ -46,7 +46,6 @@ public final class ProcessingContext { private final Elements elementUtils; private final Types typeUtils; private final ProcessingLogger logger; - private final BuilderConfiguration globalConfiguration; private final BuilderConfigurationReader configurationReader; private final ProcessingEnvironment processingEnv; private GeneratorRegistry generatorRegistry; @@ -66,7 +65,6 @@ public ProcessingContext( this.elementUtils = processingEnv.getElementUtils(); this.typeUtils = processingEnv.getTypeUtils(); this.logger = logger; - this.globalConfiguration = globalConfiguration; this.processingEnv = processingEnv; this.configurationReader = new BuilderConfigurationReader(globalConfiguration, logger, elementUtils); @@ -333,7 +331,7 @@ public void error(String format, Object... args) { * @return true if {@code -Asimplebuilder.strict=true} was supplied, false otherwise */ public boolean isStrictModeEnabled() { - return globalConfiguration.isStrictModeEnabled(); + return configurationReader.getGlobalConfiguration().isStrictModeEnabled(); } /**