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 07530341..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 @@ -116,7 +116,9 @@ public boolean process(Set annotations, RoundEnvironment try { codeGenerator.generateClass(moduleClassDef); } catch (BuilderException e) { - context.warning( + // By default Jackson module generation failures are warnings. In opt-in strict mode + // they are promoted to errors that fail the build. + context.reportBasedOnStrictMode( "simple-builders: Error generating Jackson module for package %s: %s", packageName, e.getMessage()); } @@ -169,9 +171,9 @@ 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( + // 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.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/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/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/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/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 f0fe7341..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 @@ -323,4 +323,44 @@ public void warning(Element element, String format, Object... args) { public void error(String format, Object... args) { logger.error(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 configurationReader.getGlobalConfiguration().isStrictModeEnabled(); + } + + /** + * 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 reportBasedOnStrictMode(Element element, String format, Object... args) { + if (isStrictModeEnabled()) { + logger.error(element, format, args); + } else { + logger.warning(element, format, args); + } + } + + /** + * 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 reportBasedOnStrictMode(String format, Object... args) { + if (isStrictModeEnabled()) { + logger.error(format, args); + } else { + logger.warning(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/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"); + } } 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"); + } +}