From 7b2148920f89f4b9f6bb1eaff703fc6372f3f681 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:44:15 +0000 Subject: [PATCH 1/4] feat(SimpleBuilderFor): generate builders for all classes of a package (#323) --- .../core/annotations/SimpleBuilderFor.java | 16 +- docs/CONFIGURATION.md | 13 ++ .../builders/processor/BuilderProcessor.java | 87 ++++++-- .../processing/ProcessingContext.java | 10 + .../processor/SimpleBuilderForTest.java | 190 ++++++++++++++++++ 5 files changed, 302 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java index 5cc0758f..86d928a8 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java @@ -125,11 +125,23 @@ /** * The types for which builders are generated. Every listed type must be resolvable on the * classpath or in the current compilation and must be constructible through accessible Java APIs - * (e.g. an accessible constructor). At least one type is required. + * (e.g. an accessible constructor). At least one of {@link #value()} or {@link #packages()} must + * be set. * * @return the external types to generate builders for */ - Class[] value(); + Class[] value() default {}; + + /** + * Packages scanned for types to generate builders for: every top-level constructible type + * declared in a listed package gets a builder, without listing the types one by one in {@link + * #value()}. Interfaces, abstract classes, enums and annotation types are skipped; nested types + * keep requiring an explicit {@link #value()} entry. Types a package contributes are ordered by + * qualified name. + * + * @return the packages whose top-level types get builders + */ + String[] packages() default {}; /** * Configuration options for the generated builders, reusing the {@link SimpleBuilder.Options} diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index c2406c0b..dcff692c 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -187,6 +187,19 @@ import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; The generated builders are placed in the annotated package - or in the package of the provider class when a class is annotated instead. `options` reuses `@SimpleBuilder.Options` and is optional - compiler defaults apply when omitted. +`packages` lists whole packages to scan instead of naming types one by one: every top-level constructible type declared in a listed package gets a builder, combined with the types of `value` (an explicit `value` entry wins over the same type found by scanning; a `packages`-only declaration without `value` is valid): + +```java +// Generates a builder for every top-level class and record in com.thirdparty.users +@SimpleBuilderFor( + packages = "com.thirdparty.users", + options = @SimpleBuilder.Options(packageName = "com.example.generated")) +public class ExternalBuildersProvider { +} +``` + +Interfaces, abstract classes, enums and annotation types are skipped (a debug log lists each skip), and package scanning only sees *top-level* types - nested types keep requiring an explicit `value` entry. Only packages visible to the compilation can be resolved: an unresolvable package name produces a warning and is skipped. Types a package contributes are ordered by qualified name, so the generated set is deterministic. + The target type must be constructible through accessible Java APIs from the builder's package: it needs a visible type and an accessible constructor, otherwise generation fails with a compile-time diagnostic. The target type's own annotations are not consulted - the explicit declaration wins, so `@Ignore4BuilderGeneration` on the target does not suppress generation either. `@SimpleBuilderFor` is not `@Inherited`, and the provider class or package itself never gets a builder. `@SimpleBuilderFor` is `@Repeatable`: several declarations may be placed on the same holder or `package-info.java` when the listed types need different options - for example a different `packageName` per declaration: 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 ecf98650..63addcb0 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 @@ -35,7 +35,9 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -48,8 +50,12 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template; import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; @@ -338,7 +344,7 @@ private List planGenerationOfTypeByHolder( if (targets.isEmpty()) { context.warning( holder, - "simple-builders: @SimpleBuilderFor on '%s' does not list any types - nothing to generate", + "simple-builders: @SimpleBuilderFor on '%s' resolves no types - nothing to generate", holder.getSimpleName()); continue; } @@ -392,26 +398,83 @@ private Optional planExternalTarget( } /** - * Reads the {@code value} attribute of one {@code @SimpleBuilderFor} annotation instance on the - * holder and resolves each entry to the {@link TypeElement} the builder is generated for. + * Resolves the types one {@code @SimpleBuilderFor} annotation instance declares builders for: the + * types listed in {@code value} first (a type listed there wins over the same type found by + * scanning), then the constructible top-level types of every {@code packages} package, ordered by + * qualified name for deterministic output. */ private List extractExternalTargetTypes( Element holder, AnnotationMirror annotationInstance) throws BuilderException { - Optional valueAttribute = - JavaLangAnalyser.findAnnotationAttribute(annotationInstance, "value", context); - List results = new ArrayList<>(); + Map targets = new LinkedHashMap<>(); // For an array-valued attribute javac always delivers a list, even for a single entry. - if (valueAttribute.isEmpty() || !(valueAttribute.get().getValue() instanceof List items)) { + for (Object item : readArrayAttribute(annotationInstance, "value")) { + TypeElement target = resolveExternalTargetType(holder, item); + targets.put(target.getQualifiedName().toString(), target); + } + for (String packageName : readPackageNames(annotationInstance)) { + for (TypeElement target : findPackageTargetTypes(holder, packageName)) { + targets.putIfAbsent(target.getQualifiedName().toString(), target); + } + } + return List.copyOf(targets.values()); + } + + /** Reads an array-typed attribute of an annotation mirror as the raw item list. */ + private List readArrayAttribute(AnnotationMirror annotationInstance, String attributeName) { + return JavaLangAnalyser.findAnnotationAttribute(annotationInstance, attributeName, context) + .map(AnnotationValue::getValue) + .filter(List.class::isInstance) + .map(List.class::cast) + .orElse(List.of()); + } + + /** Reads the {@code packages} attribute of one {@code @SimpleBuilderFor} annotation instance. */ + private List readPackageNames(AnnotationMirror annotationInstance) { + return readArrayAttribute(annotationInstance, "packages").stream() + .map(item -> item instanceof AnnotationValue value ? value.getValue() : null) + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + } + + /** + * Finds the constructible top-level types of one {@code packages} entry: only concrete classes + * and records qualify; interfaces, abstract classes, enums and annotation types are skipped with + * a debug note. + */ + private List findPackageTargetTypes(Element holder, String packageName) { + PackageElement packageElement = context.getPackageElement(packageName); + if (packageElement == null) { context.warning( holder, - "simple-builders: could not read the 'value' attribute of @SimpleBuilderFor on '%s' - nothing to generate", + "simple-builders: package '%s' in @SimpleBuilderFor on '%s' could not be resolved - skipped", + packageName, holder.getSimpleName()); - return results; + return List.of(); + } + List targets = new ArrayList<>(); + for (TypeElement type : ElementFilter.typesIn(packageElement.getEnclosedElements())) { + if (isConstructibleTopLevelType(type)) { + targets.add(type); + } else { + context.debug( + "simple-builders: skipping %s '%s' in package '%s' declared by @SimpleBuilderFor on '%s' - no builder can be generated", + type.getKind().toString().toLowerCase(), + type.getQualifiedName(), + packageName, + holder.getSimpleName()); + } } - for (Object item : items) { - results.add(resolveExternalTargetType(holder, item)); + targets.sort(Comparator.comparing(type -> type.getQualifiedName().toString())); + return targets; + } + + /** Whether a package member is a type a builder can be generated for. */ + private static boolean isConstructibleTopLevelType(TypeElement type) { + if (type.getKind() == ElementKind.RECORD) { + return true; } - return results; + return type.getKind() == ElementKind.CLASS && !type.getModifiers().contains(Modifier.ABSTRACT); } /** Resolves one entry of a {@code @SimpleBuilderFor} {@code value} attribute to its type. */ 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 d053a403..5af6fce0 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 @@ -273,6 +273,16 @@ public PackageElement getPackageOf(Element element) { return elementUtils.getPackageOf(element); } + /** + * Get the package element for a qualified package name. + * + * @param name the qualified package name + * @return the package element, or null if no such package is visible to the compilation + */ + public PackageElement getPackageElement(String name) { + return elementUtils.getPackageElement(name); + } + /** * Get the package name of an element. * 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 bc99e235..779bf92f 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 @@ -484,6 +484,140 @@ public class Builders {} ProcessorAsserts.assertContaining(generated, "package test;"); } + @Test + void packages_GeneratesBuildersForTopLevelConstructibleTypes() { + JavaFileObject holder = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; + @SimpleBuilderFor(packages = "pkg") + public class Builders {} + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler() + .compile(pkgUser(), pkgOrder(), pkgInterface(), pkgEnum(), pkgAbstract(), holder); + + assertThat(compilation).succeededWithoutWarnings(); + // Only the two concrete top-level classes get builders; interface, enum and abstract class + // are skipped + String userBuilder = ProcessorTestUtils.loadGeneratedSource(compilation, "ExternalUserBuilder"); + String orderBuilder = + ProcessorTestUtils.loadGeneratedSource(compilation, "ExternalOrderBuilder"); + ProcessorAsserts.assertContaining(userBuilder, "package test;", "public ExternalUser build()"); + ProcessorAsserts.assertContaining( + orderBuilder, "package test;", "public ExternalOrder build()"); + ProcessorAsserts.assertNoBuilderGenerated( + compilation, "Contract", "an interface cannot be built"); + ProcessorAsserts.assertNoBuilderGenerated(compilation, "State", "an enum cannot be built"); + ProcessorAsserts.assertNoBuilderGenerated( + compilation, "BaseDto", "an abstract class cannot be built"); + } + + @Test + void packages_NestedTypeKeepsRequiringExplicitEntry() { + JavaFileObject withNested = + ProcessorTestUtils.forSource( + """ + package pkg; + public class Wrapper { + public Wrapper() {} + public static class Inner { + public Inner() {} + } + } + """); + JavaFileObject holder = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; + @SimpleBuilderFor(packages = "pkg") + public class Builders {} + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(withNested, holder); + + assertThat(compilation).succeededWithoutWarnings(); + // Only the top-level type is picked up; its nested class is not + String generated = ProcessorTestUtils.loadGeneratedSource(compilation, "WrapperBuilder"); + ProcessorAsserts.assertContaining(generated, "public Wrapper build()"); + ProcessorAsserts.assertNoBuilderGenerated( + compilation, "Inner", "nested types keep requiring an explicit value entry"); + } + + @Test + void packages_WithOptions_GeneratesIntoOptionsPackage() { + JavaFileObject holder = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; + @SimpleBuilderFor( + packages = "pkg", + options = @SimpleBuilder.Options(packageName = "com.example.generated")) + public class Builders {} + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler().compile(pkgUser(), pkgOrder(), holder); + + assertThat(compilation).succeededWithoutWarnings(); + // Every scanned type routes into the options package, as with value-listed types + ProcessorAsserts.assertContaining( + ProcessorTestUtils.loadGeneratedSource(compilation, "generated/ExternalUserBuilder"), + "package com.example.generated;"); + ProcessorAsserts.assertContaining( + ProcessorTestUtils.loadGeneratedSource(compilation, "generated/ExternalOrderBuilder"), + "package com.example.generated;"); + } + + @Test + void packages_TypeAlsoInValue_GeneratesSingleBuilder() { + JavaFileObject holder = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; + @SimpleBuilderFor(value = pkg.ExternalUser.class, packages = "pkg") + public class Builders {} + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler().compile(pkgUser(), pkgOrder(), holder); + + // The explicit listing and the scan overlap on ExternalUser: exactly one builder, no + // duplicate warning + assertThat(compilation).succeededWithoutWarnings(); + String generated = ProcessorTestUtils.loadGeneratedSource(compilation, "ExternalUserBuilder"); + ProcessorAsserts.assertContaining(generated, "package test;", "public ExternalUser build()"); + ProcessorAsserts.assertContaining( + ProcessorTestUtils.loadGeneratedSource(compilation, "ExternalOrderBuilder"), + "public ExternalOrder build()"); + } + + @Test + void packages_Unresolvable_WarnsAndGeneratesFromValue() { + JavaFileObject holder = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; + @SimpleBuilderFor(value = ext.ExternalUser.class, packages = "does.not.exist") + public class Builders {} + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(externalDto(), holder); + + assertThat(compilation).succeeded(); + assertThat(compilation).hadWarningContaining("could not be resolved"); + // The unresolvable package is skipped; the value-listed type still generates + String generated = ProcessorTestUtils.loadGeneratedSource(compilation, "ExternalUserBuilder"); + ProcessorAsserts.assertContaining(generated, "public ExternalUser build()"); + } + private static JavaFileObject externalOrderDto() { return ProcessorTestUtils.forSource( """ @@ -526,6 +660,62 @@ public ExternalOrder() {} """); } + private static JavaFileObject pkgUser() { + return ProcessorTestUtils.forSource( + """ + package pkg; + public class ExternalUser { + private String name; + public ExternalUser() {} + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + } + + private static JavaFileObject pkgOrder() { + return ProcessorTestUtils.forSource( + """ + package pkg; + public class ExternalOrder { + private String id; + public ExternalOrder() {} + public String getId() { return id; } + public void setId(String id) { this.id = id; } + } + """); + } + + private static JavaFileObject pkgInterface() { + return ProcessorTestUtils.forSource( + """ + package pkg; + public interface Contract { + String getName(); + } + """); + } + + private static JavaFileObject pkgEnum() { + return ProcessorTestUtils.forSource( + """ + package pkg; + public enum State { + OPEN, CLOSED + } + """); + } + + private static JavaFileObject pkgAbstract() { + return ProcessorTestUtils.forSource( + """ + package pkg; + public abstract class BaseDto { + public BaseDto() {} + } + """); + } + private static JavaFileObject holder(String pkg, String className, String targetType) { return ProcessorTestUtils.forSource( """ From 1fa23dce29a18dcc58f62697eda3d3647a21d06f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:48:46 +0000 Subject: [PATCH 2/4] refactor(SimpleBuilderFor): rename packages member to sourcePackages (#323) --- .../core/annotations/SimpleBuilderFor.java | 6 +++--- docs/CONFIGURATION.md | 4 ++-- .../builders/processor/BuilderProcessor.java | 21 +++++++++++-------- .../processor/SimpleBuilderForTest.java | 20 +++++++++--------- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java index 86d928a8..07b56fae 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java @@ -125,8 +125,8 @@ /** * The types for which builders are generated. Every listed type must be resolvable on the * classpath or in the current compilation and must be constructible through accessible Java APIs - * (e.g. an accessible constructor). At least one of {@link #value()} or {@link #packages()} must - * be set. + * (e.g. an accessible constructor). At least one of {@link #value()} or {@link #sourcePackages()} + * must be set. * * @return the external types to generate builders for */ @@ -141,7 +141,7 @@ * * @return the packages whose top-level types get builders */ - String[] packages() default {}; + String[] sourcePackages() default {}; /** * Configuration options for the generated builders, reusing the {@link SimpleBuilder.Options} diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index dcff692c..d8e8cea9 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -187,12 +187,12 @@ import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; The generated builders are placed in the annotated package - or in the package of the provider class when a class is annotated instead. `options` reuses `@SimpleBuilder.Options` and is optional - compiler defaults apply when omitted. -`packages` lists whole packages to scan instead of naming types one by one: every top-level constructible type declared in a listed package gets a builder, combined with the types of `value` (an explicit `value` entry wins over the same type found by scanning; a `packages`-only declaration without `value` is valid): +`sourcePackages` lists whole packages to scan instead of naming types one by one: every top-level constructible type declared in a listed package gets a builder, combined with the types of `value` (an explicit `value` entry wins over the same type found by scanning; a `sourcePackages`-only declaration without `value` is valid). It names the packages the types come *from* - not to be confused with `options.packageName`, the package builders are generated *into*: ```java // Generates a builder for every top-level class and record in com.thirdparty.users @SimpleBuilderFor( - packages = "com.thirdparty.users", + sourcePackages = "com.thirdparty.users", options = @SimpleBuilder.Options(packageName = "com.example.generated")) public class ExternalBuildersProvider { } 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 63addcb0..deced551 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 @@ -400,8 +400,8 @@ private Optional planExternalTarget( /** * Resolves the types one {@code @SimpleBuilderFor} annotation instance declares builders for: the * types listed in {@code value} first (a type listed there wins over the same type found by - * scanning), then the constructible top-level types of every {@code packages} package, ordered by - * qualified name for deterministic output. + * scanning), then the constructible top-level types of every {@code sourcePackages} package, + * ordered by qualified name for deterministic output. */ private List extractExternalTargetTypes( Element holder, AnnotationMirror annotationInstance) throws BuilderException { @@ -411,7 +411,7 @@ private List extractExternalTargetTypes( TypeElement target = resolveExternalTargetType(holder, item); targets.put(target.getQualifiedName().toString(), target); } - for (String packageName : readPackageNames(annotationInstance)) { + for (String packageName : readSourcePackageNames(annotationInstance)) { for (TypeElement target : findPackageTargetTypes(holder, packageName)) { targets.putIfAbsent(target.getQualifiedName().toString(), target); } @@ -428,9 +428,12 @@ private List readArrayAttribute(AnnotationMirror annotationInstance, String a .orElse(List.of()); } - /** Reads the {@code packages} attribute of one {@code @SimpleBuilderFor} annotation instance. */ - private List readPackageNames(AnnotationMirror annotationInstance) { - return readArrayAttribute(annotationInstance, "packages").stream() + /** + * Reads the {@code sourcePackages} attribute of one {@code @SimpleBuilderFor} annotation + * instance. + */ + private List readSourcePackageNames(AnnotationMirror annotationInstance) { + return readArrayAttribute(annotationInstance, "sourcePackages").stream() .map(item -> item instanceof AnnotationValue value ? value.getValue() : null) .filter(String.class::isInstance) .map(String.class::cast) @@ -438,9 +441,9 @@ private List readPackageNames(AnnotationMirror annotationInstance) { } /** - * Finds the constructible top-level types of one {@code packages} entry: only concrete classes - * and records qualify; interfaces, abstract classes, enums and annotation types are skipped with - * a debug note. + * Finds the constructible top-level types of one {@code sourcePackages} entry: only concrete + * classes and records qualify; interfaces, abstract classes, enums and annotation types are + * skipped with a debug note. */ private List findPackageTargetTypes(Element holder, String packageName) { PackageElement packageElement = context.getPackageElement(packageName); 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 779bf92f..8c1d49e5 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 @@ -485,13 +485,13 @@ public class Builders {} } @Test - void packages_GeneratesBuildersForTopLevelConstructibleTypes() { + void sourcePackages_GeneratesBuildersForTopLevelConstructibleTypes() { JavaFileObject holder = ProcessorTestUtils.forSource( """ package test; import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; - @SimpleBuilderFor(packages = "pkg") + @SimpleBuilderFor(sourcePackages = "pkg") public class Builders {} """); @@ -516,7 +516,7 @@ public class Builders {} } @Test - void packages_NestedTypeKeepsRequiringExplicitEntry() { + void sourcePackages_NestedTypeKeepsRequiringExplicitEntry() { JavaFileObject withNested = ProcessorTestUtils.forSource( """ @@ -533,7 +533,7 @@ public Inner() {} """ package test; import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; - @SimpleBuilderFor(packages = "pkg") + @SimpleBuilderFor(sourcePackages = "pkg") public class Builders {} """); @@ -548,7 +548,7 @@ public class Builders {} } @Test - void packages_WithOptions_GeneratesIntoOptionsPackage() { + void sourcePackages_WithOptions_GeneratesIntoOptionsPackage() { JavaFileObject holder = ProcessorTestUtils.forSource( """ @@ -556,7 +556,7 @@ void packages_WithOptions_GeneratesIntoOptionsPackage() { import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; @SimpleBuilderFor( - packages = "pkg", + sourcePackages = "pkg", options = @SimpleBuilder.Options(packageName = "com.example.generated")) public class Builders {} """); @@ -575,13 +575,13 @@ public class Builders {} } @Test - void packages_TypeAlsoInValue_GeneratesSingleBuilder() { + void sourcePackages_TypeAlsoInValue_GeneratesSingleBuilder() { JavaFileObject holder = ProcessorTestUtils.forSource( """ package test; import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; - @SimpleBuilderFor(value = pkg.ExternalUser.class, packages = "pkg") + @SimpleBuilderFor(value = pkg.ExternalUser.class, sourcePackages = "pkg") public class Builders {} """); @@ -599,13 +599,13 @@ public class Builders {} } @Test - void packages_Unresolvable_WarnsAndGeneratesFromValue() { + void sourcePackages_Unresolvable_WarnsAndGeneratesFromValue() { JavaFileObject holder = ProcessorTestUtils.forSource( """ package test; import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; - @SimpleBuilderFor(value = ext.ExternalUser.class, packages = "does.not.exist") + @SimpleBuilderFor(value = ext.ExternalUser.class, sourcePackages = "does.not.exist") public class Builders {} """); From 2e3eeda15e29e04593aa89d3fe5156ef2f0d4db1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:49:49 +0000 Subject: [PATCH 3/4] docs(SimpleBuilderFor): disambiguate sourcePackages from options.packageName (#323) --- .../simple/builders/core/annotations/SimpleBuilderFor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java index 07b56fae..2e55ad4c 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderFor.java @@ -137,7 +137,8 @@ * declared in a listed package gets a builder, without listing the types one by one in {@link * #value()}. Interfaces, abstract classes, enums and annotation types are skipped; nested types * keep requiring an explicit {@link #value()} entry. Types a package contributes are ordered by - * qualified name. + * qualified name. These are the packages the types come from — the package the builders + * are generated into is {@link SimpleBuilder.Options#packageName()}. * * @return the packages whose top-level types get builders */ From 5b816c7a2f9326bc3a67b7d6de8685ed776d1bda Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:58:31 +0000 Subject: [PATCH 4/4] test(SimpleBuilderFor): cover multiple sourcePackages entries (#323) --- .../processor/SimpleBuilderForTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) 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 8c1d49e5..665107c6 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 @@ -598,6 +598,44 @@ public class Builders {} "public ExternalOrder build()"); } + @Test + void sourcePackages_Multiple_GeneratesFromAllListedPackages() { + JavaFileObject otherType = + ProcessorTestUtils.forSource( + """ + package other; + public class ExternalCustomer { + private String id; + public ExternalCustomer() {} + public String getId() { return id; } + public void setId(String id) { this.id = id; } + } + """); + JavaFileObject holder = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; + @SimpleBuilderFor(sourcePackages = {"pkg", "other"}) + public class Builders {} + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler().compile(pkgUser(), pkgOrder(), otherType, holder); + + assertThat(compilation).succeededWithoutWarnings(); + // Every listed package contributes its top-level classes into the holder's package + ProcessorAsserts.assertContaining( + ProcessorTestUtils.loadGeneratedSource(compilation, "ExternalUserBuilder"), + "import pkg.ExternalUser;"); + ProcessorAsserts.assertContaining( + ProcessorTestUtils.loadGeneratedSource(compilation, "ExternalOrderBuilder"), + "import pkg.ExternalOrder;"); + ProcessorAsserts.assertContaining( + ProcessorTestUtils.loadGeneratedSource(compilation, "ExternalCustomerBuilder"), + "import other.ExternalCustomer;"); + } + @Test void sourcePackages_Unresolvable_WarnsAndGeneratesFromValue() { JavaFileObject holder =