diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 68a39142..858f157b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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. @@ -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. 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 b2c96064..f8efc32c 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 @@ -86,10 +86,10 @@ public BuilderScopeResolver(ProcessingContext context) { *
  • 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}. * * * @param referencedType the type element being referenced as a field or collection element @@ -199,10 +199,11 @@ private Optional 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 @@ -217,6 +218,9 @@ private Optional 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(); } 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 f82c557f..9d24179c 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 @@ -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();