Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -745,8 +745,8 @@ case.

The processor constructs the candidate builder name using `builderUsageSuffix`
(or `builderSuffix` if not configured) and verifies the builder contract: a
constructor accepting the referenced type and a no-arg `build()` method returning
it. Any class with the expected name and a matching contract qualifies, allowing
constructor accepting the referenced type, a no-arg constructor, and a no-arg
`build()` method returning it. Any class with the expected name and a matching contract qualifies, allowing
references to builders generated with custom template annotations, external tools,
or different suffixes. If the candidate builder cannot be found, the field falls
back to a plain setter.
Expand Down Expand Up @@ -1023,8 +1023,8 @@ If empty, `builderSuffix` is used instead. This allows referencing builders that
with a different suffix (e.g. by another module using `"Factory"` as suffix) without changing
the suffix used for own builder generation.

The candidate class must provide a constructor accepting the referenced type and a no-arg
`build()` method returning it. The contract check is annotation-agnostic, so builders
The candidate class must provide a constructor accepting the referenced type, a no-arg
constructor, and a no-arg `build()` method returning it. The contract check is annotation-agnostic, so builders
generated with custom template annotations or external tools are supported. If the
candidate class does not exist or does not satisfy this contract, the field falls back
to a plain setter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ public BuilderScopeResolver(ProcessingContext context) {
* <li>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
* the referenced type and a no-arg {@code build()} method returning it. The contract check
* is annotation-agnostic, so builders generated with custom template annotations, external
* tools, or different suffixes are supported. The referenced type must not be opted out
* with {@code @Ignore4BuilderGeneration}.
* the referenced type, a no-arg constructor, and a no-arg {@code build()} method returning
* it. The contract check is annotation-agnostic, so builders generated with custom template
* annotations, external tools, or different suffixes are supported. The referenced type
* must not be opted out with {@code @Ignore4BuilderGeneration}.
* </ol>
*
* @param referencedType the type element being referenced as a field or collection element
Expand Down Expand Up @@ -199,10 +199,11 @@ private Optional<TypeName> resolve(TypeElement referencedType) {

/**
* Looks up the candidate builder type on the classpath and verifies it satisfies the builder
* contract: a constructor accepting the referenced type and a no-arg {@code build()} method
* returning it. The contract check is annotation-agnostic, so builders generated with custom
* template annotations or from external sources are supported as long as they follow the builder
* contract. It also avoids false positives like {@code String} → {@code StringBuilder}.
* contract: a constructor accepting the referenced type, a no-arg constructor, and a no-arg
* {@code build()} method returning it. The contract check is annotation-agnostic, so builders
* generated with custom template annotations or from external sources are supported as long as
* they follow the builder contract. It also avoids false positives like {@code String} → {@code
* StringBuilder}.
*
* @param candidate the candidate builder type name to look up
* @param expectedType the qualified name of the referenced type the builder must accept and
Expand All @@ -217,6 +218,9 @@ private Optional<TypeName> resolveByBuilderContract(TypeName candidate, String e
if (!JavaLangAnalyser.hasConstructorAccepting(builderTypeElement, expectedType, context)) {
return Optional.empty();
}
if (!JavaLangAnalyser.hasEmptyConstructor(builderTypeElement, context)) {
return Optional.empty();
}
if (!JavaLangAnalyser.hasBuildMethodReturning(builderTypeElement, expectedType, context)) {
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,33 @@ public LibHelperBuilder(LibHelper value) {}
ResolverProbeProcessor.usageWithoutAnnotation.get().getFullQualifiedName());
}

@Test
void resolverUsageScope_RejectsBuilderWithoutNoArgConstructor() {
ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
.compile(
ProcessorTestUtils.forSource(
"""
package lib;
public class LibHelper { public LibHelper() {} }
"""),
ProcessorTestUtils.forSource(
"""
package lib;
public class LibHelperBuilder {
public LibHelperBuilder(LibHelper value) {}
public LibHelper build() { return new LibHelper(); }
}
"""));

assertThat(compilation).succeeded();
// ctor(T) + build() but no no-arg ctor: generated consumer code calls `new
// LibHelperBuilder()`, so the builder must not qualify
assertEquals(Optional.empty(), ResolverProbeProcessor.usageWithoutAnnotation);
}

@Test
void resolverUsageScope_UsesBuilderUsageSuffixWhenConfigured() {
ResolverProbeProcessor.reset();
Expand Down
Loading