From e1f39c2703312735451b6dcbd2ef7b0f38ed1e2d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:34:48 +0000 Subject: [PATCH 1/3] feat(options): add packageName option for generated builders Adds a new packageName member to @SimpleBuilder.Options that places generated builders into a configured package instead of the default one (the annotated type's package, resp. the @SimpleBuilderFor holder's package). - Effective package flows through ProcessingTarget.builderPackage, so builder TypeName, accessibility checks and file placement all follow the configured package consistently. - Members not reachable from the configured package (package-private and protected constructors/setters/getters) are treated as absent. - Invalid package names are rejected at compile time via SourceVersion.isName with a diagnostic on the affected element. - Also available as compiler option -Asimplebuilder.packageName. Resolves java-helpers/simple-builders#294 Co-Authored-By: Andreas Igel --- .../core/annotations/SimpleBuilder.java | 19 +++++ docs/CONFIGURATION.md | 26 ++++++ .../builders/processor/BuilderProcessor.java | 49 +++++++++--- .../model/core/BuilderConfiguration.java | 20 +++++ .../processing/CompilerArgumentsEnum.java | 3 + .../processing/ProcessingTarget.java | 9 ++- .../processor/BuilderProcessorTest.java | 24 ++++++ .../processor/SimpleBuilderForTest.java | 80 +++++++++++++++++++ 8 files changed, 215 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java index 28276eaa..d919c7ac 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java @@ -689,6 +689,25 @@ */ String jacksonModulePackage() default ""; + /** + * The package generated builder classes are placed in.
+ * Useful to collect builders in a central package or to generate a builder for a type whose + * package is not suitable, e.g. a third-party package. + * + *

Example: {@code "com.example.generated"} + * + *

Only members of the target type that are accessible from the configured package are used + * for builder generation - package-private and protected constructors, setters and getters are + * then not reachable and are treated as if absent. + * + *

Default: "" (empty - the package of the annotated type, resp. of the element carrying + * {@code @SimpleBuilderFor})
+ * Compiler option: -Asimplebuilder.packageName + * + * @return the package generated builders are placed in + */ + String packageName() default ""; + /** * Generate Javadoc comments on the generated builder class and its members.
* When disabled, no class, field, constructor or method Javadoc is emitted, producing smaller diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e5c5a008..732b214f 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1045,6 +1045,32 @@ public class OwnerDto { --- +#### `packageName` + +**Default**: `""` (empty — the package of the annotated type, resp. of the element carrying +`@SimpleBuilderFor`) | **Compiler Option**: `-Asimplebuilder.packageName=com.example.generated` + +Places generated builder classes into the given package instead of the default package. +Useful to collect builders in a central package, or to keep a third-party target's builder +in your own namespace when using `@SimpleBuilderFor`. + +Only members of the target type that are accessible from the configured package are used for +builder generation — package-private and protected constructors, setters and getters are +then not reachable and are treated as if absent. The value must be a valid Java package +name; an invalid value produces a compile-time diagnostic. + +**Example**: +```java +@SimpleBuilderFor( + value = ExternalUser.class, + options = @SimpleBuilder.Options(packageName = "com.example.generated")) +public class ExternalBuilders { } + +// Generated class: com.example.generated.ExternalUserBuilder +``` + +--- + #### `setterSuffix` **Default**: `""` (empty) | **Compiler Option**: `-Asimplebuilder.setterSuffix=customPrefix` 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 e4bd7dc9..468dff5a 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 @@ -304,15 +304,17 @@ private Optional planGenerationOfAnnotatedElement( if (!context.getBuilderScopeResolver().isInGenerationScope(annotatedElement, config)) { return Optional.empty(); } + validateBuilderPackage(annotatedElement, config); alreadyPlannedBuilders.add( - builderTypeName(annotatedElement, context.getPackageName(annotatedElement), config)); + builderTypeName( + annotatedElement, effectiveBuilderPackage(annotatedElement, config), config)); return Optional.of(new ElementToGenerate(annotatedElement, config, annotatedElement)); } /** * Expands a {@code @SimpleBuilderFor} holder into the external types listed in its {@code value} - * attribute and plans a builder for each of them. The generated builder is placed in the holder's - * package. + * attribute and plans a builder for each of them. The generated builder is placed in the + * configured {@code packageName} or the holder's package. */ private List planGenerationOfTypeByHolder( Element holder, @@ -334,7 +336,8 @@ private List planGenerationOfTypeByHolder( BuilderConfiguration config = reader.resolveHolderConfiguration(holder); tracker.endPhase(PHASE_CONFIGURATION_RESOLUTION); - String builderPackage = context.getPackageName(holder); + validateBuilderPackage(holder, config); + String builderPackage = effectiveBuilderPackage(holder, config); List results = new ArrayList<>(); for (TypeElement target : targets) { planExternalTarget(target, holder, config, builderPackage, alreadyPlannedBuilders) @@ -441,9 +444,11 @@ private void registerGeneratedTypes(List elementsToGenerate) } scopeResolver.registerGeneratedBuilder( new TypeName(context.getPackageName(targetType), targetType.getSimpleName().toString()), - new TypeName( - context.getPackageName(elementToGenerate.reportingElement()), - targetType.getSimpleName() + elementToGenerate.config().getBuilderSuffix())); + builderTypeName( + targetType, + effectiveBuilderPackage( + elementToGenerate.reportingElement(), elementToGenerate.config()), + elementToGenerate.config())); } } @@ -545,12 +550,34 @@ private record ElementToGenerate( Element element, BuilderConfiguration config, Element reportingElement) {} /** - * The package the builder is generated into: the package of the element the generation is - * reported on - the {@code @SimpleBuilderFor} holder for external types, the annotated type - * itself otherwise. + * The package the builder is generated into: the configured {@code packageName} option when set, + * otherwise the package of the element the generation is reported on - the + * {@code @SimpleBuilderFor} holder for external types, the annotated type itself otherwise. */ private String builderPackageOf(ElementToGenerate elementToGenerate) { - return context.getPackageName(elementToGenerate.reportingElement()); + return effectiveBuilderPackage( + elementToGenerate.reportingElement(), elementToGenerate.config()); + } + + /** + * The effective builder package: the configured {@code packageName} option when set, otherwise + * the package of the given element. + */ + private String effectiveBuilderPackage(Element reportingElement, BuilderConfiguration config) { + String configured = config.getPackageName(); + return configured == null ? context.getPackageName(reportingElement) : configured; + } + + /** + * Ensures the configured {@code packageName} option is a syntactically valid Java package name. + */ + private void validateBuilderPackage(Element element, BuilderConfiguration config) + throws BuilderException { + String packageName = config.getPackageName(); + if (packageName != null && !SourceVersion.isName(packageName)) { + throw new BuilderException( + element, "Option packageName '%s' is not a valid Java package name", packageName); + } } /** 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 fac8a9af..14a8f7b9 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 @@ -106,6 +106,7 @@ public record BuilderConfiguration( OptionState generateJacksonModule, OptionState generateJavaDoc, String jacksonModulePackage, + String packageName, PackageScopes builderGenerationPackages, PackageScopes builderUsagePackages, String builderSuffix, @@ -263,6 +264,16 @@ public String getJacksonModulePackage() { return jacksonModulePackage; } + /** + * The package generated builders are placed in, or {@code null} when the default applies (the + * package of the annotated type, resp. of the {@code @SimpleBuilderFor} holder). + * + * @return the configured builder package, or {@code null} if unset + */ + public String getPackageName() { + return packageName; + } + /** * Returns the parsed set of builder generation package scopes. * @@ -373,6 +384,7 @@ public BuilderConfiguration merge(BuilderConfiguration other) { mergeOptionState(other.generateJacksonModule, this.generateJacksonModule)) .generateJavaDoc(mergeOptionState(other.generateJavaDoc, this.generateJavaDoc)) .jacksonModulePackage(mergeString(other.jacksonModulePackage, this.jacksonModulePackage)) + .packageName(mergeString(other.packageName, this.packageName)) .builderGenerationPackages( overrideScopes(other.builderGenerationPackages, this.builderGenerationPackages)) .builderUsagePackages(overrideScopes(other.builderUsagePackages, this.builderUsagePackages)) @@ -462,6 +474,7 @@ public String toString() { .appendValueIfSet("generateJacksonModule", generateJacksonModule) .appendValueIfSet("generateJavaDoc", generateJavaDoc) .appendIfNotEmpty("jacksonModulePackage", jacksonModulePackage) + .appendIfNotEmpty("packageName", packageName) .appendIfNotEmpty("builderGenerationPackages", builderGenerationPackages.toString()) .appendIfNotEmpty("builderUsagePackages", builderUsagePackages.toString()) .appendIfNotEmpty("builderSuffix", builderSuffix) @@ -546,6 +559,7 @@ public static class Builder { private OptionState generateJacksonModule = OptionState.UNSET; private OptionState generateJavaDoc = OptionState.UNSET; private String jacksonModulePackage = null; + private String packageName = null; // === Naming === private String builderSuffix = null; @@ -648,6 +662,11 @@ public Builder jacksonModulePackage(String value) { return this; } + public Builder packageName(String value) { + this.packageName = StringUtils.trimToNull(value); + return this; + } + public Builder builderGenerationPackages(String value) { this.builderGenerationPackages = PackageScopes.parse(value); return this; @@ -896,6 +915,7 @@ public BuilderConfiguration build() { generateJacksonModule, generateJavaDoc, jacksonModulePackage, + packageName, builderGenerationPackages, builderUsagePackages, builderSuffix, 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 7e89eb14..d5b9be98 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 @@ -144,6 +144,9 @@ public enum CompilerArgumentsEnum { /** Option for Jackson Module package name. */ JACKSON_MODULE_PACKAGE("jacksonModulePackage", string(Builder::jacksonModulePackage)), + /** Option for the package generated builders are placed in. */ + PACKAGE_NAME("packageName", string(Builder::packageName)), + // === Builder Scoping === /** Option for builder generation packages. */ BUILDER_GENERATION_PACKAGES( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java index 4ec0d01a..9a4c5059 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java @@ -34,9 +34,10 @@ * can reach it without threading both values through every call. * * @param configuration the resolved builder configuration for the current target - * @param builderPackage the package the generated builder is written to. For {@code @SimpleBuilder} - * targets this is the processed type's own package; for {@code @SimpleBuilderFor} targets it is - * the holder's package. The latter cannot be derived from the processed element itself, which - * is why it is carried explicitly here + * @param builderPackage the package the generated builder is written to. This is the configured + * {@code packageName} option when set; otherwise the processed type's own package for + * {@code @SimpleBuilder} targets, resp. the holder's package for {@code @SimpleBuilderFor} + * targets. It cannot always be derived from the processed element itself, which is why it is + * carried explicitly here */ public record ProcessingTarget(BuilderConfiguration configuration, String builderPackage) {} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java index b3cb6aa2..707bd3e4 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java @@ -3696,4 +3696,28 @@ public class GenericSetterDto { assertThat(compilation) .hadWarningContaining("Field 'value' has field-specific generics, so it will be ignored"); } + + @Test + void packageName_GeneratesBuilderIntoConfiguredPackage() { + JavaFileObject source = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + @SimpleBuilder(options = @SimpleBuilder.Options(packageName = "com.example.generated")) + public class Person { + private String name; + public Person() {} + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(source); + + assertThat(compilation).succeededWithoutWarnings(); + String generated = loadGeneratedSource(compilation, "PersonBuilder"); + ProcessorAsserts.assertContaining( + generated, "package com.example.generated;", "import test.Person;"); + } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java index fa2d85af..471f8884 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java @@ -317,6 +317,86 @@ void packageInfo_GeneratesBuildersIntoAnnotatedPackage() { generated, "package test;", "public ExternalUserBuilder name(String name)"); } + @Test + void packageName_GeneratesBuilderIntoConfiguredPackage() { + JavaFileObject holder = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; + @SimpleBuilderFor( + value = ext.ExternalUser.class, + options = @SimpleBuilder.Options(packageName = "com.example.generated")) + public class Builders {} + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(externalDto(), holder); + + assertThat(compilation).succeededWithoutWarnings(); + String generated = ProcessorTestUtils.loadGeneratedSource(compilation, "ExternalUserBuilder"); + ProcessorAsserts.assertContaining( + generated, + "package com.example.generated;", + "import ext.ExternalUser;", + "public ExternalUserBuilder name(String name)"); + } + + @Test + void packageName_Invalid_ProducesDiagnosticAndNoBuilder() { + JavaFileObject holder = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; + @SimpleBuilderFor( + value = ext.ExternalUser.class, + options = @SimpleBuilder.Options(packageName = "com.example..broken")) + public class Builders {} + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(externalDto(), holder); + + assertThat(compilation).succeeded(); + assertThat(compilation).hadWarningContaining("is not a valid Java package name"); + ProcessorAsserts.assertNoBuilderGenerated( + compilation, "ExternalUserBuilder", "An invalid packageName must not produce a builder"); + } + + @Test + void packageName_PackagePrivateConstructorIsNotReachable() { + // With the builder placed in another package, package-private members of the target are no + // longer reachable - they are treated as if absent, so no accessible constructor remains. + JavaFileObject packagePrivateCtor = + ProcessorTestUtils.forSource( + """ + package ext; + public class ExternalWidget { + ExternalWidget() {} + } + """); + JavaFileObject holder = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; + @SimpleBuilderFor( + value = ext.ExternalWidget.class, + options = @SimpleBuilder.Options(packageName = "com.example.generated")) + public class Builders {} + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler().compile(packagePrivateCtor, holder); + + assertThat(compilation).succeeded(); + assertThat(compilation).hadWarningContaining("No accessible constructor"); + ProcessorAsserts.assertNoBuilderGenerated( + compilation, "ExternalWidgetBuilder", "No accessible constructor - no builder"); + } + private static JavaFileObject externalDto() { return ProcessorTestUtils.forSource( """ From 2a18a3025442e82d5c3037fa88056ccc120495e5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 19:43:09 +0000 Subject: [PATCH 2/3] refactor(options): restrict packageName to annotation usage A global -Asimplebuilder.packageName / -Dsimplebuilder.packageName would collapse all generated builders into a single package, so the option is now declared annotationOnly and ignored on the compiler-arg path. Co-Authored-By: Andreas Igel --- .../core/annotations/SimpleBuilder.java | 2 +- docs/CONFIGURATION.md | 3 +- .../processing/CompilerArgumentsEnum.java | 35 +++++++++++++++++-- .../processing/CompilerArgumentsReader.java | 4 ++- .../processor/SimpleBuilderForTest.java | 14 ++++++++ 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java index d919c7ac..6b671ac0 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java @@ -702,7 +702,7 @@ * *

Default: "" (empty - the package of the annotated type, resp. of the element carrying * {@code @SimpleBuilderFor})
- * Compiler option: -Asimplebuilder.packageName + * Annotation-only: a global value would collapse all generated builders into a single package. * * @return the package generated builders are placed in */ diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 732b214f..68a39142 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1048,7 +1048,8 @@ public class OwnerDto { #### `packageName` **Default**: `""` (empty — the package of the annotated type, resp. of the element carrying -`@SimpleBuilderFor`) | **Compiler Option**: `-Asimplebuilder.packageName=com.example.generated` +`@SimpleBuilderFor`) | **Annotation only** — a global `-Asimplebuilder.packageName` is +deliberately not supported: it would collapse all generated builders into a single package. Places generated builder classes into the given package instead of the default package. Useful to collect builders in a central package, or to keep a third-party target's builder 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 d5b9be98..41cc7277 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 @@ -144,8 +144,11 @@ public enum CompilerArgumentsEnum { /** Option for Jackson Module package name. */ JACKSON_MODULE_PACKAGE("jacksonModulePackage", string(Builder::jacksonModulePackage)), - /** Option for the package generated builders are placed in. */ - PACKAGE_NAME("packageName", string(Builder::packageName)), + /** + * Option for the package generated builders are placed in. Annotation-only: a global {@code + * packageName} would collapse all builders into a single package. + */ + PACKAGE_NAME("packageName", string(Builder::packageName), true), // === Builder Scoping === /** Option for builder generation packages. */ @@ -217,6 +220,12 @@ public enum CompilerArgumentsEnum { */ private final OptionApplier builderApplier; + /** + * Whether this option may only be set per annotation and is ignored in compiler arguments ({@code + * -Asimplebuilder.*} / {@code -D}). + */ + private final boolean annotationOnly; + /** * Constructs a CompilerArgumentsEnum constant for an argument that is not a builder configuration * option. @@ -247,8 +256,20 @@ public enum CompilerArgumentsEnum { * @param builderApplier applies the raw option value to the configuration builder */ CompilerArgumentsEnum(String optionName, OptionApplier builderApplier) { + this(optionName, builderApplier, false); + } + + /** + * Constructs a CompilerArgumentsEnum constant for a builder configuration option. + * + * @param optionName The option name + * @param builderApplier applies the raw option value to the configuration builder + * @param annotationOnly {@code true} to restrict the option to annotation usage + */ + CompilerArgumentsEnum(String optionName, OptionApplier builderApplier, boolean annotationOnly) { this.optionName = optionName; this.builderApplier = builderApplier; + this.annotationOnly = annotationOnly; } /** @@ -307,6 +328,16 @@ public boolean hasValueApplier() { return builderApplier != null; } + /** + * Returns whether this option is only valid as an annotation member and is ignored when set + * through compiler arguments. + * + * @return {@code true} if the option must not be read from compiler arguments + */ + public boolean isAnnotationOnly() { + return annotationOnly; + } + /** * Applies a raw option value to the given configuration builder. * 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 8ad1efbb..42ed27ab 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 @@ -117,7 +117,8 @@ public boolean readBooleanValue(CompilerArgumentsEnum argument) { * {@code BuilderConfigurationReader}. The remaining checklist when adding a new option: * *

    - *
  1. {@code CompilerArgumentsEnum} — add the enum constant with its applier. + *
  2. {@code CompilerArgumentsEnum} — add the enum constant with its applier (or pass {@code + * annotationOnly = true} if the option must not be set globally). *
  3. {@code BuilderConfiguration} — add the field, builder method, merge logic, and a typed * accessor (e.g. {@code formattingModeEnum}) if enum conversion is needed. Set the default * in {@code BuilderConfiguration.DEFAULT}. @@ -133,6 +134,7 @@ public boolean readBooleanValue(CompilerArgumentsEnum argument) { public BuilderConfiguration readBuilderConfiguration(ProcessingLogger logger) { BuilderConfiguration.Builder builder = BuilderConfiguration.builder(); Stream.of(CompilerArgumentsEnum.values()) + .filter(option -> !option.isAnnotationOnly()) .forEach(option -> option.apply(builder, readValue(option), logger)); return builder.build(); } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java index 471f8884..b89982e7 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java @@ -397,6 +397,20 @@ public class Builders {} compilation, "ExternalWidgetBuilder", "No accessible constructor - no builder"); } + @Test + void packageName_GlobalCompilerArgumentIsIgnored() { + // packageName is annotation-only: a global -A/-D value must not collapse all builders into + // a single package. + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions("-Asimplebuilder.packageName=com.example.generated") + .compile(externalDto(), holder("test", "Builders", "ext.ExternalUser")); + + assertThat(compilation).succeededWithoutWarnings(); + String generated = ProcessorTestUtils.loadGeneratedSource(compilation, "ExternalUserBuilder"); + ProcessorAsserts.assertContaining(generated, "package test;"); + } + private static JavaFileObject externalDto() { return ProcessorTestUtils.forSource( """ From adfe74d65c29583a59356f7fd78a597481bafece Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:24:10 +0000 Subject: [PATCH 3/3] refactor: validate packageName during configuration resolution Moves the packageName validity check into BuilderConfigurationReader next to validateAccessModifiers - the resolved configuration is validated once for @SimpleBuilder targets and @SimpleBuilderFor holders. Co-Authored-By: Andreas Igel --- .../builders/processor/BuilderProcessor.java | 14 ------------- .../BuilderConfigurationReader.java | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 14 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 468dff5a..6fcb5bed 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 @@ -304,7 +304,6 @@ private Optional planGenerationOfAnnotatedElement( if (!context.getBuilderScopeResolver().isInGenerationScope(annotatedElement, config)) { return Optional.empty(); } - validateBuilderPackage(annotatedElement, config); alreadyPlannedBuilders.add( builderTypeName( annotatedElement, effectiveBuilderPackage(annotatedElement, config), config)); @@ -336,7 +335,6 @@ private List planGenerationOfTypeByHolder( BuilderConfiguration config = reader.resolveHolderConfiguration(holder); tracker.endPhase(PHASE_CONFIGURATION_RESOLUTION); - validateBuilderPackage(holder, config); String builderPackage = effectiveBuilderPackage(holder, config); List results = new ArrayList<>(); for (TypeElement target : targets) { @@ -568,18 +566,6 @@ private String effectiveBuilderPackage(Element reportingElement, BuilderConfigur return configured == null ? context.getPackageName(reportingElement) : configured; } - /** - * Ensures the configured {@code packageName} option is a syntactically valid Java package name. - */ - private void validateBuilderPackage(Element element, BuilderConfiguration config) - throws BuilderException { - String packageName = config.getPackageName(); - if (packageName != null && !SourceVersion.isName(packageName)) { - throw new BuilderException( - element, "Option packageName '%s' is not a valid Java package name", packageName); - } - } - /** * Checks whether the provided SourceVersion is at least Java 17 in a backwards compatible way. */ 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 d82d8b4e..f25ae6c5 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 @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; @@ -137,6 +138,7 @@ public BuilderConfiguration resolveConfiguration(Element element) throws Builder // Validate access modifiers and warn about problematic configurations validateAccessModifiers(element, result); + validatePackageName(element, result); logger.debugEndOperation("Resulting configuration resolved: %s", result.toString()); return result; @@ -170,6 +172,7 @@ public BuilderConfiguration resolveHolderConfiguration(Element element) throws B // Validate access modifiers and warn about problematic configurations validateAccessModifiers(element, result); + validatePackageName(element, result); logger.debugEndOperation("Resulting configuration resolved: %s", result.toString()); return result; @@ -427,4 +430,21 @@ private static void validateAccessModifiers(Element element, BuilderConfiguratio elementName); } } + + /** + * Validates the {@code packageName} option and throws an exception for values that are not a + * valid Java package name. + * + * @param element the element being processed + * @param config the resolved configuration + * @throws BuilderException if the configured package name is invalid + */ + private static void validatePackageName(Element element, BuilderConfiguration config) + throws BuilderException { + String packageName = config.getPackageName(); + if (packageName != null && !SourceVersion.isName(packageName)) { + throw new BuilderException( + element, "Option packageName '%s' is not a valid Java package name", packageName); + } + } }