diff --git a/example/generated-example-builder/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilder.java b/example/generated-example-builder/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilder.java
index d098f5fb..9557e1d1 100644
--- a/example/generated-example-builder/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilder.java
+++ b/example/generated-example-builder/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilder.java
@@ -14,6 +14,7 @@
import org.javahelpers.simple.builders.core.util.BuilderToStringStyle;
import org.javahelpers.simple.builders.core.util.TrackedValue;
import org.javahelpers.simple.builders.example.SponsorDto;
+import org.javahelpers.simple.builders.example.SponsorDtoBuilder;
import org.javahelpers.simple.builders.example.library.LibraryHelperDto;
/**
@@ -32,6 +33,7 @@
* .library(LibraryHelperDto::new)
* .sponsor(new SponsorDto())
* .sponsor(SponsorDto::new)
+ * .sponsor(sponsorDtoBuilder -> sponsorDtoBuilder)
* .trusted(new TrustedHelperDto())
* .trusted(TrustedHelperDto::new)
* .trusted(trustedHelperDtoBuilder -> trustedHelperDtoBuilder)
@@ -180,17 +182,25 @@ public ScopedOwnerDtoBuilder sponsor(SponsorDto sponsor) {
}
/**
- * Sets the value for sponsor by executing the provided consumer.
+ * Sets the value for sponsor using a builder consumer that produces the value.
*
* Generated from setter {@link ScopedOwnerDto#setSponsor(SponsorDto) setSponsor(SponsorDto sponsor)}
*
- * @param sponsorConsumer consumer providing an instance of sponsor
+ *
Example:
+ *
+ * {@code
+ * builder.sponsor(sponsorDtoBuilder -> sponsorDtoBuilder);
+ * }
+ *
+ * @param sponsorBuilderConsumer consumer providing an instance of a builder for sponsor
* @return current instance of builder
*/
- public ScopedOwnerDtoBuilder sponsor(Consumer sponsorConsumer) {
- SponsorDto consumer = this.sponsor.isSet() ? this.sponsor.value() : new SponsorDto();
- sponsorConsumer.accept(consumer);
- this.sponsor = changedValue(consumer);
+ public ScopedOwnerDtoBuilder sponsor(Consumer sponsorBuilderConsumer) {
+ SponsorDtoBuilder builder = this.sponsor.isSet()
+ ? new SponsorDtoBuilder(this.sponsor.value())
+ : new SponsorDtoBuilder();
+ sponsorBuilderConsumer.accept(builder);
+ this.sponsor = changedValue(builder.build());
return this;
}
@@ -217,7 +227,8 @@ public ScopedOwnerDtoBuilder sponsor(Supplier sponsorSupplier) {
* Updates the current value of sponsor in place by applying the given operator, instead of reading it
* out, changing it and setting it again. Useful for adjustments relative to the current value, e.g. trimming,
* upper-casing, clamping or incrementing, and in combination with the With copy-and-modify flow. The
- * value must have been set before (directly or via an existing instance).
+ * value must have been set before (directly or via an existing instance). For changing multiple values of a nested
+ * DTO, prefer the builder-consumer helper {@link #sponsor(Consumer)}.
*
* Generated from setter {@link ScopedOwnerDto#setSponsor(SponsorDto) setSponsor(SponsorDto sponsor)}
*
diff --git a/example/src/test/java/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilderTest.java b/example/src/test/java/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilderTest.java
index f7c6bee8..14ed7c12 100644
--- a/example/src/test/java/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilderTest.java
+++ b/example/src/test/java/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilderTest.java
@@ -45,7 +45,9 @@ void exposesScopedBuilderConsumerOverloads() {
assertFalse(
hasBuilderConsumerMethod(
"library", "org.javahelpers.simple.builders.example.library.LibraryHelperDtoBuilder"));
- assertFalse(hasBuilderConsumerMethod("sponsor", SponsorDtoBuilder.class.getName()));
+ // SponsorDto's builder is generated in the same compilation, so it is trusted and used
+ // regardless of the usage scope
+ assertTrue(hasBuilderConsumerMethod("sponsor", SponsorDtoBuilder.class.getName()));
assertTrue(hasMethod("trusted", TrustedHelperDto.class));
assertTrue(hasMethod("library", LibraryHelperDto.class));
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
index 0f9bc265..dac1431f 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
@@ -80,12 +80,13 @@ public BuilderScopeResolver(ProcessingContext context) {
*
The decision follows these rules:
*
*
+ * - If the referenced type's builder is generated in the current processing round (registered
+ * via {@link #registerGeneratedTypes}), the candidate builder (using {@code builderSuffix})
+ * is returned immediately — trusted without a classpath lookup or contract check, and
+ * exempt from the usage scope: a builder this processor generates is always used.
*
- If the usage scope is set and the referenced type's package is not in it, no builder may
* be referenced. The usage scope includes generation-scope packages automatically. When the
* scope is empty, any package is allowed (backward compatibility).
- *
- If the referenced type's builder is generated in the current processing round (registered
- * via {@link #registerGeneratedTypes}), the candidate builder (using {@code builderSuffix})
- * is returned immediately — trusted without a classpath lookup or contract check.
*
- Otherwise, the candidate builder name is constructed using {@code builderUsageSuffix}
* (falling back to {@code builderSuffix} if not configured). The candidate is looked up on
* the classpath and returned if it satisfies the builder contract: a constructor accepting
@@ -162,17 +163,11 @@ private Optional resolve(TypeElement referencedType) {
String referencedTypeFqn = referencedType.getQualifiedName().toString();
String packageName = context.getPackageName(referencedType);
- // The usage scope determines whether a type is eligible to be referenced as a builder
- // helper. When empty, any package is allowed (backward compatibility). When set, only
- // packages in the scope qualify. The scope already includes generation-scope packages.
- if (!usagePackages.isEmpty() && !usagePackages.includes(packageName)) {
- return Optional.empty();
- }
-
// Types whose builders are generated in the current processing round are trusted
// immediately — our own generators always produce the builder contract, so no
- // classpath lookup or contract check is needed. The candidate uses builderSuffix
- // because that is what our own generators produce.
+ // classpath lookup or contract check is needed. Being generated by this processor also
+ // makes them exempt from the usage scope: an explicitly generated builder is always used.
+ // The candidate uses builderSuffix because that is what our own generators produce.
if (generatedTypeNames.contains(referencedTypeFqn)) {
TypeName candidate =
JavaLangMapper.createBuilderTypeName(
@@ -180,6 +175,13 @@ private Optional resolve(TypeElement referencedType) {
return Optional.of(candidate);
}
+ // The usage scope determines whether a type is eligible to be referenced as a builder
+ // helper. When empty, any package is allowed (backward compatibility). When set, only
+ // packages in the scope qualify. The scope already includes generation-scope packages.
+ if (!usagePackages.isEmpty() && !usagePackages.includes(packageName)) {
+ return Optional.empty();
+ }
+
// For types not generated in this round, look up the candidate on the classpath using
// builderUsageSuffix (which falls back to builderSuffix if not configured) and verify
// the builder contract.
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java
index c7bcd27c..2fcb2a2e 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java
@@ -117,8 +117,11 @@ public class LibHelper { public LibHelper() {} }
assertThat(compilation).succeeded();
// With usage scope "other" (not "lib") and no registration → empty
assertEquals(Optional.empty(), ResolverProbeProcessor.beforeRegistration);
- // Registration alone is not enough — the type must also be in the usage scope
- assertEquals(Optional.empty(), ResolverProbeProcessor.afterRegistration);
+ // Registered types are trusted and exempt from the usage scope — a builder this
+ // processor generates is always used
+ assertEquals(
+ "lib.LibHelperBuilder",
+ ResolverProbeProcessor.afterRegistration.get().getFullQualifiedName());
// With usage scope "lib" but no registration → empty (builder not on classpath)
assertEquals(Optional.empty(), ResolverProbeProcessor.usageBeforeRegistration);
// With usage scope "lib" AND registration → builder resolved