second;
From be6bbf6f5b6c12d482ab813736953592c73bbbeb Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sat, 3 Oct 2026 13:11:09 +0000
Subject: [PATCH 07/12] Add usingExistingBuilders option gating reuse of
non-generated builders
---
.../core/annotations/SimpleBuilder.java | 15 +++++
.../annotations/SimpleMinimalBuilder.java | 1 +
docs/CONFIGURATION.md | 13 +++++
.../analysis/BuilderScopeResolver.java | 7 +++
.../model/core/BuilderConfiguration.java | 23 ++++++++
.../processing/CompilerArgumentsEnum.java | 3 +
.../processor/BuilderScopeResolverTest.java | 56 +++++++++++++++++++
7 files changed, 118 insertions(+)
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 6b671ac0..7ff9b14e 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
@@ -770,6 +770,21 @@
*/
String builderUsagePackages() default "";
+ /**
+ * Reuse builders that were not generated by this processor: classpath builders discovered
+ * through {@link #builderUsagePackages()} or builders anchored inside the referenced type
+ * itself (a nested {@code Person.Builder} or a {@code Person.builder()} factory method).
+ * When disabled, only builders generated in the current processing round qualify and every
+ * other referenced type falls back to a plain setter - opting out of false-positive detections
+ * on types that coincidentally look like builders.
+ *
+ * Default: ENABLED
+ * Compiler option: -Asimplebuilder.usingExistingBuilders
+ *
+ * @return the option state for reusing existing builders
+ */
+ OptionState usingExistingBuilders() default OptionState.UNSET;
+
// === Naming ===
/**
* Suffix to append to the DTO name to generate the builder class name.
diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleMinimalBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleMinimalBuilder.java
index c6df4cf7..54d4777a 100644
--- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleMinimalBuilder.java
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleMinimalBuilder.java
@@ -94,6 +94,7 @@
generateJavaDoc = OptionState.DISABLED,
copyTypeAnnotations = OptionState.DISABLED,
implementsBuilderBase = OptionState.DISABLED,
+ usingExistingBuilders = OptionState.DISABLED,
builderSuffix = "Builder",
setterSuffix = "",
formattingMode = "lightweight"))
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 31bc9194..2c595c04 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -779,6 +779,19 @@ The `example` module contains a runnable demo in package
It demonstrates a generation-scope builder consumer, a usage-scope missing-builder fallback, and
an out-of-scope plain setter in [`ScopedOwnerDtoBuilder.java`](../example/generated-example-builder/org/javahelpers/simple/builders/example/scoping/ScopedOwnerDtoBuilder.java).
+#### `usingExistingBuilders`
+
+**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.usingExistingBuilders=DISABLED`
+
+Controls whether builders that were not generated by this processor may be
+reused — both the classpath lookup described above (usage scope, in-type
+anchors like `Person.builder()` or a nested `Person.Builder`) and any other
+existing builder satisfying the contract. When disabled, only builders
+generated in the current processing round qualify and every other referenced
+type falls back to a plain setter, opting out of false-positive detections on
+types that coincidentally look like builders. `@SimpleMinimalBuilder` disables
+this option.
+
### Component Filtering
#### `deactivateGenerationComponents`
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 7ee0b99e..723795b5 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
@@ -220,6 +220,13 @@ private Optional resolve(TypeElement referencedType) {
new BuilderInstantiation.ConstructorCall()));
}
+ // Reusing builders not generated in this round - nested or anchored inside the referenced
+ // type, or looked up on the classpath - is opt-out via usingExistingBuilders, so a type
+ // coincidentally looking like a builder cannot be picked up unwillingly.
+ if (!context.getConfiguration().shouldUseExistingBuilders()) {
+ return Optional.empty();
+ }
+
// A type may anchor its builder inside itself: an accessible static parameterless method
// returning a contract-satisfying type (MapStruct-style `Person.builder()`). The type's own
// declaration wins over the same-package candidate below.
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 14a8f7b9..8d98f3dd 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
@@ -71,6 +71,8 @@
* @param builderUsagePackages Additional packages whose builders may be used as helpers (unscoped =
* all packages if builderGenerationPackages is unscoped as well, otherwise only the generation
* scope is usable)
+ * @param usingExistingBuilders Reuse builders not generated by this processor (classpath lookup and
+ * in-type anchors); when disabled only same-round generated builders qualify
* @param builderSuffix Suffix for builder class name
* @param builderUsageSuffix Suffix for builder class name when referencing builders from the usage
* scope; null means fall back to {@code builderSuffix}
@@ -109,6 +111,7 @@ public record BuilderConfiguration(
String packageName,
PackageScopes builderGenerationPackages,
PackageScopes builderUsagePackages,
+ OptionState usingExistingBuilders,
String builderSuffix,
String builderUsageSuffix,
String setterSuffix,
@@ -152,6 +155,7 @@ public record BuilderConfiguration(
.jacksonModulePackage(null)
.builderGenerationPackages(PackageScopes.unscoped())
.builderUsagePackages(PackageScopes.unscoped())
+ .usingExistingBuilders(ENABLED)
.builderSuffix("Builder")
.setterSuffix("")
.formattingMode(FormattingMode.JDT.getOptionValue())
@@ -292,6 +296,10 @@ public Set getBuilderUsagePackagesSet() {
return builderUsagePackages.packages();
}
+ public boolean shouldUseExistingBuilders() {
+ return usingExistingBuilders == ENABLED;
+ }
+
public String getBuilderSuffix() {
return builderSuffix;
}
@@ -388,6 +396,8 @@ public BuilderConfiguration merge(BuilderConfiguration other) {
.builderGenerationPackages(
overrideScopes(other.builderGenerationPackages, this.builderGenerationPackages))
.builderUsagePackages(overrideScopes(other.builderUsagePackages, this.builderUsagePackages))
+ .usingExistingBuilders(
+ mergeOptionState(other.usingExistingBuilders, this.usingExistingBuilders))
.builderSuffix(mergeString(other.builderSuffix, this.builderSuffix))
.builderUsageSuffix(mergeString(other.builderUsageSuffix, this.builderUsageSuffix))
.setterSuffix(mergeString(other.setterSuffix, this.setterSuffix))
@@ -477,6 +487,7 @@ public String toString() {
.appendIfNotEmpty("packageName", packageName)
.appendIfNotEmpty("builderGenerationPackages", builderGenerationPackages.toString())
.appendIfNotEmpty("builderUsagePackages", builderUsagePackages.toString())
+ .appendValueIfSet("usingExistingBuilders", usingExistingBuilders)
.appendIfNotEmpty("builderSuffix", builderSuffix)
.appendIfNotEmpty("builderUsageSuffix", builderUsageSuffix)
.appendIfNotEmpty("setterSuffix", setterSuffix)
@@ -572,6 +583,7 @@ public static class Builder {
// === Builder Scoping ===
private PackageScopes builderGenerationPackages = null;
private PackageScopes builderUsagePackages = null;
+ private OptionState usingExistingBuilders = OptionState.UNSET;
// === Error Handling ===
private OptionState strict = OptionState.UNSET;
@@ -687,6 +699,16 @@ public Builder builderUsagePackages(PackageScopes value) {
return this;
}
+ public Builder usingExistingBuilders(OptionState value) {
+ this.usingExistingBuilders = value;
+ return this;
+ }
+
+ public Builder usingExistingBuilders(boolean value) {
+ this.usingExistingBuilders = value ? ENABLED : DISABLED;
+ return this;
+ }
+
public Builder generateVarArgsHelpers(OptionState value) {
this.generateVarArgsHelpers = value;
return this;
@@ -918,6 +940,7 @@ public BuilderConfiguration build() {
packageName,
builderGenerationPackages,
builderUsagePackages,
+ usingExistingBuilders,
builderSuffix,
builderUsageSuffix,
setterSuffix,
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 41cc7277..70bcc60d 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
@@ -158,6 +158,9 @@ public enum CompilerArgumentsEnum {
/** Option for builder usage packages. */
BUILDER_USAGE_PACKAGES("builderUsagePackages", string(Builder::builderUsagePackages)),
+ /** Option for reusing builders not generated by this processor. */
+ USING_EXISTING_BUILDERS("usingExistingBuilders", optionState(Builder::usingExistingBuilders)),
+
// === Naming ===
/** Option for builder class name suffix. */
BUILDER_SUFFIX("builderSuffix", string(Builder::builderSuffix)),
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 287d1eae..5ec79704 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
@@ -38,6 +38,7 @@
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
+import org.javahelpers.simple.builders.core.enums.OptionState;
import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
import org.javahelpers.simple.builders.processor.model.type.BuilderInstantiation;
@@ -672,6 +673,41 @@ public LibHelperBuilder(LibHelper value) {}
BuilderInstantiation.ConstructorCall.class, resolved.funcForPrefilledBuilder());
}
+ @Test
+ void resolverUsageScope_UsingExistingBuildersDisabled() {
+ 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() {}
+ public LibHelperBuilder(LibHelper value) {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ """));
+
+ assertThat(compilation).succeeded();
+ // usingExistingBuilders=DISABLED: the contract-satisfying classpath builder is not reused
+ assertEquals(Optional.empty(), ResolverProbeProcessor.usageWithExistingDisabled);
+ // ... but a builder generated in the current round is own generation and still resolves
+ ResolvedBuilder generated = ResolverProbeProcessor.usageGeneratedWithExistingDisabled.get();
+ assertEquals("lib.LibHelperBuilder", generated.typeName().getFullQualifiedName());
+ assertInstanceOf(BuilderInstantiation.StaticFactoryCall.class, generated.funcForEmptyBuilder());
+ assertInstanceOf(
+ BuilderInstantiation.ConstructorCall.class, generated.funcForPrefilledBuilder());
+ }
+
private static final class ResolverProbeProcessor extends AbstractProcessor {
private static Optional first;
private static Optional second;
@@ -684,6 +720,8 @@ private static final class ResolverProbeProcessor extends AbstractProcessor {
private static Optional usageWithSuffix;
private static Optional usageDefaultSuffix;
private static Optional usagePackagePrivate;
+ private static Optional usageWithExistingDisabled;
+ private static Optional usageGeneratedWithExistingDisabled;
private boolean captured;
@@ -699,6 +737,8 @@ static void reset() {
usageWithSuffix = null;
usageDefaultSuffix = null;
usagePackagePrivate = null;
+ usageWithExistingDisabled = null;
+ usageGeneratedWithExistingDisabled = null;
}
@Override
@@ -761,6 +801,14 @@ public boolean process(Set extends TypeElement> annotations, RoundEnvironment
// contract members are accessible
context.initProcessingTarget(new ProcessingTarget(usageOnlyConfiguration("lib"), "lib"));
usagePackagePrivate = resolver.resolveUsableBuilderType(helper);
+ // usingExistingBuilders=DISABLED: existing builders are not reused, but a builder
+ // generated in the current round is own generation and still resolves
+ context.initProcessingTarget(
+ new ProcessingTarget(existingBuildersDisabledConfiguration("lib"), ""));
+ usageWithExistingDisabled = resolver.resolveUsableBuilderType(helper);
+ resolver.registerGeneratedBuilder(
+ new TypeName("lib", "LibHelper"), new TypeName("lib", "LibHelperBuilder"));
+ usageGeneratedWithExistingDisabled = resolver.resolveUsableBuilderType(helper);
captured = true;
return false;
}
@@ -778,6 +826,14 @@ private static BuilderConfiguration usageOnlyConfiguration(String packageName) {
BuilderConfiguration.builder().builderUsagePackages(packageName).build());
}
+ private static BuilderConfiguration existingBuildersDisabledConfiguration(String packageName) {
+ return BuilderConfiguration.DEFAULT.merge(
+ BuilderConfiguration.builder()
+ .builderUsagePackages(packageName)
+ .usingExistingBuilders(OptionState.DISABLED)
+ .build());
+ }
+
private static BuilderConfiguration usageWithSuffixConfiguration(
String packageName, String usageSuffix) {
return BuilderConfiguration.DEFAULT.merge(
From 72a2a7ba6e17914a9a606465016ebba7837da9ee Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sat, 3 Oct 2026 13:29:04 +0000
Subject: [PATCH 08/12] Use StringUtils.removeStart in mapToTypeName; move
probe reset to @BeforeEach
---
.../processor/analysis/JavaLangMapper.java | 4 +--
.../processor/BuilderScopeResolverTest.java | 27 +++++--------------
2 files changed, 8 insertions(+), 23 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
index 7bfcec9c..db6d9b24 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
@@ -39,6 +39,7 @@
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.SimpleTypeVisitor14;
+import org.apache.commons.lang3.StringUtils;
import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto;
import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto;
import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto;
@@ -142,8 +143,7 @@ public static TypeName mapToTypeName(TypeElement typeElement, ProcessingContext
}
String packageName = context.getPackageName(typeElement);
String qualifiedName = typeElement.getQualifiedName().toString();
- String className =
- packageName.isEmpty() ? qualifiedName : qualifiedName.substring(packageName.length() + 1);
+ String className = StringUtils.removeStart(qualifiedName, packageName + ".");
return new TypeName(packageName, className);
}
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 5ec79704..ed70aa48 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
@@ -48,6 +48,7 @@
import org.javahelpers.simple.builders.processor.processing.ProcessingTarget;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
@@ -58,9 +59,13 @@
*/
class BuilderScopeResolverTest {
+ @BeforeEach
+ void resetProbe() {
+ ResolverProbeProcessor.reset();
+ }
+
@Test
void resolverReturnsEmptyAfterConfigurationChangesToExcludePackage() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -87,7 +92,6 @@ public class LibHelper { public LibHelper() {} }
@Test
void resolverCachesResolvedOptionalInstancePerReferencedType() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -107,7 +111,6 @@ public class LibHelper { public LibHelper() {} }
@Test
void resolverClearsCacheOnRegistrationAndResolvesUsageScope() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -135,7 +138,6 @@ public class LibHelper { public LibHelper() {} }
@Test
void resolverUsageScope_ResolvesBuilderWithoutSimpleBuilderAnnotation() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -164,7 +166,6 @@ public LibHelperBuilder(LibHelper value) {}
@Test
void resolverUsageScope_RejectsBuilderWithoutNoArgConstructor() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -191,7 +192,6 @@ public LibHelperBuilder(LibHelper value) {}
@Test
void resolverUsageScope_RejectsBuilderWithPrivateConstructor() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -219,7 +219,6 @@ public LibHelperBuilder(LibHelper value) {}
@Test
void resolverUsageScope_AcceptsBuilderWithStaticFactories() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -258,7 +257,6 @@ private LibHelperBuilder() {}
@Test
void resolverUsageScope_PackagePrivateMembers_AccessibleOnlyFromSamePackage() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -290,7 +288,6 @@ public class LibHelperBuilder {
@Test
void resolverUsageScope_UsesBuilderUsageSuffixWhenConfigured() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -319,7 +316,6 @@ public LibHelperFactory(LibHelper value) {}
@Test
void resolverUsageScope_FallsBackToBuilderSuffixWhenUsageSuffixNotSet() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -350,7 +346,6 @@ public LibHelperBuilder(LibHelper value) {}
@Test
void resolverUsageScope_ResolvesBuilderAnchoredInsideType() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -390,7 +385,6 @@ public LibHelperBuilder(lib.LibHelper value) {}
@Test
void resolverUsageScope_IgnoresInTypeAnchorWithoutContract() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -419,7 +413,6 @@ public NoContract() {}
@Test
void resolverUsageScope_PrefersBuilderNamedInTypeFactory() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -464,7 +457,6 @@ public LibHelperBuilder(lib.LibHelper value) {}
@Test
void resolverUsageScope_ResolvesNestedTypeBuilder() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -492,7 +484,6 @@ public LibHelperBuilder(LibHelper value) {}
@Test
void resolverUsageScope_ResolvesLombokBuilderShape() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -524,7 +515,6 @@ public static class LibHelperBuilder {
@Test
void resolverUsageScope_LombokBuilderShapeWithoutToBuilderFallsBack() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -549,7 +539,6 @@ public static class LibHelperBuilder {
@Test
void resolverUsageScope_FreeBuilderShapeFallsBack() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -575,7 +564,6 @@ public Builder() {}
@Test
void resolverUsageScope_AutoValueShapeFallsBack() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -608,7 +596,6 @@ public static class Builder {
@Test
void resolverUsageScope_ImmutablesShapeFallsBack() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -641,7 +628,6 @@ public Builder() {}
@Test
void resolverUsageScope_ResolvesRecordBuilderShape() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
@@ -675,7 +661,6 @@ public LibHelperBuilder(LibHelper value) {}
@Test
void resolverUsageScope_UsingExistingBuildersDisabled() {
- ResolverProbeProcessor.reset();
Compilation compilation =
Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
From 7efcdb3dc17195e8d49d1abbcaff9c166026c9e7 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sat, 3 Oct 2026 13:39:58 +0000
Subject: [PATCH 09/12] Address Sonar: extract candidate ranking, parameterize
contract-variant tests
---
.../analysis/BuilderScopeResolver.java | 22 +-
.../processor/BuilderScopeResolverTest.java | 285 +++++++++---------
2 files changed, 167 insertions(+), 140 deletions(-)
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 723795b5..16aa198b 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
@@ -352,14 +352,28 @@ private List nestedBuilderCandidates(TypeElement referencedType) {
.filter(context::isMemberAccessibleFromBuilderPackage)
.sorted(
Comparator.comparingInt(
- nested ->
- preferredName.contentEquals(nested.getSimpleName())
- ? 0
- : builderSuffix.contentEquals(nested.getSimpleName()) ? 1 : 2))
+ nested -> nestedCandidateRank(nested, preferredName, builderSuffix)))
.map(nested -> JavaLangMapper.mapToTypeName(nested, context))
.toList();
}
+ /**
+ * Ranks a nested builder candidate by name: {@code } first, the plain
+ * builder suffix second, all other names last.
+ *
+ * @param nested the nested type to rank
+ * @param preferredName the preferred candidate name
+ * @param builderSuffix the builder suffix
+ * @return the rank, lower wins
+ */
+ private static int nestedCandidateRank(
+ Element nested, String preferredName, String builderSuffix) {
+ if (preferredName.contentEquals(nested.getSimpleName())) {
+ return 0;
+ }
+ return builderSuffix.contentEquals(nested.getSimpleName()) ? 1 : 2;
+ }
+
/**
* Resolves a builder obtained through a static parameterless factory method anchored on the
* referenced type itself (e.g. {@code Person.builder()}). The factory is the instantiation path;
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 ed70aa48..032baf95 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
@@ -34,6 +34,8 @@
import com.google.testing.compile.Compiler;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
@@ -50,6 +52,9 @@
import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
/**
* Probe-based coverage of {@link BuilderScopeResolver} internals that generated-source assertions
@@ -136,154 +141,162 @@ public class LibHelper { public LibHelper() {} }
ResolverProbeProcessor.usageAfterRegistration.get().typeName().getFullQualifiedName());
}
- @Test
- void resolverUsageScope_ResolvesBuilderWithoutSimpleBuilderAnnotation() {
- 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() {}
- public LibHelperBuilder(LibHelper value) {}
- public LibHelper build() { return new LibHelper(); }
- }
- """));
+ private static final String LIB_HELPER =
+ """
+ package lib;
+ public class LibHelper { public LibHelper() {} }
+ """;
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("resolvedContractVariants")
+ void resolverUsageScope_ResolvesContractVariants(
+ String name,
+ String builderSource,
+ Supplier> probeResult,
+ String expectedBuilder,
+ Class> expectedEmptyPath,
+ String expectedEmptyMethod,
+ Class> expectedPrefilledPath,
+ String expectedPrefilledMethod) {
+ Compilation compilation = compileWithBuilder(builderSource);
assertThat(compilation).succeeded();
- // Usage scope without @SimpleBuilder annotation — builder resolved by contract check
- assertEquals(
- "lib.LibHelperBuilder",
- ResolverProbeProcessor.usageWithoutAnnotation.get().typeName().getFullQualifiedName());
+ ResolvedBuilder resolved = probeResult.get().get();
+ assertEquals(expectedBuilder, resolved.typeName().getFullQualifiedName());
+ BuilderInstantiation empty = resolved.funcForEmptyBuilder();
+ BuilderInstantiation prefilled = resolved.funcForPrefilledBuilder();
+ assertInstanceOf(expectedEmptyPath, empty);
+ assertInstanceOf(expectedPrefilledPath, prefilled);
+ assertFactoryMethod(expectedEmptyMethod, empty);
+ assertFactoryMethod(expectedPrefilledMethod, prefilled);
}
- @Test
- void resolverUsageScope_RejectsBuilderWithoutNoArgConstructor() {
- 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);
+ static Stream resolvedContractVariants() {
+ return Stream.of(
+ // Usage scope without @SimpleBuilder annotation — builder resolved by contract check
+ Arguments.of(
+ "BuilderWithoutSimpleBuilderAnnotation",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ public LibHelperBuilder() {}
+ public LibHelperBuilder(LibHelper value) {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usageWithoutAnnotation),
+ "lib.LibHelperBuilder",
+ BuilderInstantiation.ConstructorCall.class,
+ null,
+ BuilderInstantiation.ConstructorCall.class,
+ null),
+ // No accessible constructors: both instantiation paths come from the static factories
+ Arguments.of(
+ "StaticFactories",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ private LibHelperBuilder() {}
+ public static LibHelperBuilder create() { return new LibHelperBuilder(); }
+ public static LibHelperBuilder of(LibHelper value) { return create(); }
+ public LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usageWithoutAnnotation),
+ "lib.LibHelperBuilder",
+ BuilderInstantiation.StaticFactoryCall.class,
+ "create",
+ BuilderInstantiation.StaticFactoryCall.class,
+ "of"),
+ // Package-private contract members are accessible when the generated builder is in
+ // the same package (builderPackage "lib")
+ Arguments.of(
+ "PackagePrivateMembers_SamePackage",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ LibHelperBuilder() {}
+ LibHelperBuilder(LibHelper value) {}
+ LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usagePackagePrivate),
+ "lib.LibHelperBuilder",
+ BuilderInstantiation.ConstructorCall.class,
+ null,
+ BuilderInstantiation.ConstructorCall.class,
+ null));
}
- @Test
- void resolverUsageScope_RejectsBuilderWithPrivateConstructor() {
- Compilation compilation =
- Compiler.javac()
- .withProcessors(new ResolverProbeProcessor())
- .compile(
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class LibHelper { public LibHelper() {} }
- """),
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class LibHelperBuilder {
- private LibHelperBuilder() {}
- public LibHelperBuilder(LibHelper value) {}
- public LibHelper build() { return new LibHelper(); }
- }
- """));
-
- assertThat(compilation).succeeded();
- // The no-arg ctor exists but is private: generated code could not call it, so the builder
- // must not qualify
- assertEquals(Optional.empty(), ResolverProbeProcessor.usageWithoutAnnotation);
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("rejectedContractVariants")
+ void resolverUsageScope_RejectsContractVariants(
+ String name, String builderSource, Supplier> probeResult) {
+ assertThat(compileWithBuilder(builderSource)).succeeded();
+ assertEquals(Optional.empty(), probeResult.get());
}
- @Test
- void resolverUsageScope_AcceptsBuilderWithStaticFactories() {
- Compilation compilation =
- Compiler.javac()
- .withProcessors(new ResolverProbeProcessor())
- .compile(
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class LibHelper { public LibHelper() {} }
- """),
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class LibHelperBuilder {
- private LibHelperBuilder() {}
- public static LibHelperBuilder create() { return new LibHelperBuilder(); }
- public static LibHelperBuilder of(LibHelper value) { return create(); }
- public LibHelper build() { return new LibHelper(); }
- }
- """));
+ static Stream rejectedContractVariants() {
+ return Stream.of(
+ // ctor(T) + build() but no no-arg ctor: generated consumer code calls `new
+ // LibHelperBuilder()`, so the builder must not qualify
+ Arguments.of(
+ "BuilderWithoutNoArgConstructor",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ public LibHelperBuilder(LibHelper value) {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usageWithoutAnnotation)),
+ // The no-arg ctor exists but is private: generated code could not call it
+ Arguments.of(
+ "BuilderWithPrivateConstructor",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ private LibHelperBuilder() {}
+ public LibHelperBuilder(LibHelper value) {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usageWithoutAnnotation)),
+ // Package-private contract members are not accessible from a different package
+ // (builderPackage unset)
+ Arguments.of(
+ "PackagePrivateMembers_OtherPackage",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ LibHelperBuilder() {}
+ LibHelperBuilder(LibHelper value) {}
+ LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usageWithoutAnnotation)));
+ }
- assertThat(compilation).succeeded();
- // No accessible constructors: both instantiation paths come from the static factories
- ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get();
- assertEquals("lib.LibHelperBuilder", resolved.typeName().getFullQualifiedName());
- assertEquals(
- "create",
- assertInstanceOf(
- BuilderInstantiation.StaticFactoryCall.class, resolved.funcForEmptyBuilder())
- .methodName());
- assertEquals(
- "of",
- assertInstanceOf(
- BuilderInstantiation.StaticFactoryCall.class, resolved.funcForPrefilledBuilder())
- .methodName());
+ private static Compilation compileWithBuilder(String builderSource) {
+ return Compiler.javac()
+ .withProcessors(new ResolverProbeProcessor())
+ .compile(
+ ProcessorTestUtils.forSource(LIB_HELPER), ProcessorTestUtils.forSource(builderSource));
}
- @Test
- void resolverUsageScope_PackagePrivateMembers_AccessibleOnlyFromSamePackage() {
- Compilation compilation =
- Compiler.javac()
- .withProcessors(new ResolverProbeProcessor())
- .compile(
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class LibHelper { public LibHelper() {} }
- """),
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class LibHelperBuilder {
- LibHelperBuilder() {}
- LibHelperBuilder(LibHelper value) {}
- LibHelper build() { return new LibHelper(); }
- }
- """));
+ private static Supplier> probe(
+ Supplier> field) {
+ return field;
+ }
- assertThat(compilation).succeeded();
- // All contract members are package-private: accessible when the generated builder is in the
- // same package (builderPackage "lib")...
- assertEquals(
- "lib.LibHelperBuilder",
- ResolverProbeProcessor.usagePackagePrivate.get().typeName().getFullQualifiedName());
- // ...but not from a different one (builderPackage unset)
- assertEquals(Optional.empty(), ResolverProbeProcessor.usageWithoutAnnotation);
+ private static void assertFactoryMethod(
+ String expectedMethod, BuilderInstantiation instantiation) {
+ if (expectedMethod != null) {
+ assertEquals(
+ expectedMethod,
+ assertInstanceOf(BuilderInstantiation.StaticFactoryCall.class, instantiation)
+ .methodName());
+ }
}
@Test
From e75acf58e9f56ff40b5e65e85e903320b9b76c19 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sat, 3 Oct 2026 13:45:52 +0000
Subject: [PATCH 10/12] Cover seeded-path ranking branches; drop unused
findMethods overload
---
.../processor/analysis/JavaLangAnalyser.java | 14 ----
.../processor/BuilderScopeResolverTest.java | 76 ++++++++++++++++++-
2 files changed, 74 insertions(+), 16 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java
index df4a686c..2d6d9fb8 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java
@@ -309,20 +309,6 @@ public static List findMethodsStatic(
.toList();
}
- /**
- * Finds accessible methods on the given type taking the expected parameter types, regardless of
- * their return type and whether they are static.
- *
- * @param type the type element to inspect
- * @param expectedParameterTypes the required parameter types
- * @param context the processing context, used to access all members
- * @return the matching methods, in declaration order
- */
- public static List findMethods(
- TypeElement type, List expectedParameterTypes, ProcessingContext context) {
- return findMethods(type, expectedParameterTypes, mirror -> true, context);
- }
-
/**
* Finds accessible methods on the given type taking the expected parameter types and returning
* the expected return type, whether they are static or instance methods.
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 032baf95..165e3fdc 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
@@ -190,7 +190,8 @@ public LibHelperBuilder(LibHelper value) {}
null,
BuilderInstantiation.ConstructorCall.class,
null),
- // No accessible constructors: both instantiation paths come from the static factories
+ // No accessible constructors: both instantiation paths come from the static
+ // factories — the second empty factory exercises the preferred-name ranking
Arguments.of(
"StaticFactories",
"""
@@ -198,6 +199,7 @@ public LibHelperBuilder(LibHelper value) {}
public class LibHelperBuilder {
private LibHelperBuilder() {}
public static LibHelperBuilder create() { return new LibHelperBuilder(); }
+ public static LibHelperBuilder make() { return create(); }
public static LibHelperBuilder of(LibHelper value) { return create(); }
public LibHelper build() { return new LibHelper(); }
}
@@ -468,6 +470,72 @@ public LibHelperBuilder(lib.LibHelper value) {}
ResolverProbeProcessor.usageWithoutAnnotation.get().typeName().getFullQualifiedName());
}
+ @Test
+ void resolverUsageScope_ResolvesStaticSeededPath() {
+ Compilation compilation =
+ Compiler.javac()
+ .withProcessors(new ResolverProbeProcessor())
+ .compile(
+ ProcessorTestUtils.forSource(
+ """
+ package lib;
+ public class LibHelper {
+ public static LibHelperBuilder builder() { return new LibHelperBuilder(); }
+ public static LibHelperBuilder builder(LibHelper value) {
+ return new LibHelperBuilder();
+ }
+ public static LibHelperBuilder builder2(LibHelper value) {
+ return new LibHelperBuilder();
+ }
+ public static class LibHelperBuilder {
+ private LibHelperBuilder() {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ }
+ """));
+
+ assertThat(compilation).succeeded();
+ // No seeded path on the builder itself: the type's static builder(T) becomes the seeded
+ // path — two candidates match, the conventional name wins
+ ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get();
+ assertEquals("lib.LibHelper.LibHelperBuilder", resolved.typeName().getFullQualifiedName());
+ assertInstanceOf(BuilderInstantiation.AnchorFactoryCall.class, resolved.funcForEmptyBuilder());
+ BuilderInstantiation.AnchorFactoryCall prefilled =
+ assertInstanceOf(
+ BuilderInstantiation.AnchorFactoryCall.class, resolved.funcForPrefilledBuilder());
+ assertEquals("builder", prefilled.methodName());
+ }
+
+ @Test
+ void resolverUsageScope_InstanceSeededPathPrefersToBuilder() {
+ Compilation compilation =
+ Compiler.javac()
+ .withProcessors(new ResolverProbeProcessor())
+ .compile(
+ ProcessorTestUtils.forSource(
+ """
+ package lib;
+ public class LibHelper {
+ public static LibHelperBuilder builder() { return new LibHelperBuilder(); }
+ public LibHelperBuilder copy() { return new LibHelperBuilder(); }
+ public LibHelperBuilder toBuilder() { return new LibHelperBuilder(); }
+ public static class LibHelperBuilder {
+ LibHelperBuilder() {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ }
+ """));
+
+ assertThat(compilation).succeeded();
+ // Two instance methods return the builder type; the conventional toBuilder name wins
+ ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get();
+ assertEquals("lib.LibHelper.LibHelperBuilder", resolved.typeName().getFullQualifiedName());
+ BuilderInstantiation.ValueFactoryCall prefilled =
+ assertInstanceOf(
+ BuilderInstantiation.ValueFactoryCall.class, resolved.funcForPrefilledBuilder());
+ assertEquals("toBuilder", prefilled.methodName());
+ }
+
@Test
void resolverUsageScope_ResolvesNestedTypeBuilder() {
Compilation compilation =
@@ -483,11 +551,15 @@ public LibHelperBuilder() {}
public LibHelperBuilder(LibHelper value) {}
public LibHelper build() { return new LibHelper(); }
}
+ public static class Other {
+ public Other() {}
+ }
}
"""));
assertThat(compilation).succeeded();
- // The nested type satisfies the full contract and wins over any anchored method
+ // The preferred-name nested type satisfies the full contract and wins over any anchored
+ // method; the second nested type exercises the candidate ranking
ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get();
assertEquals("lib.LibHelper.LibHelperBuilder", resolved.typeName().getFullQualifiedName());
assertInstanceOf(BuilderInstantiation.ConstructorCall.class, resolved.funcForEmptyBuilder());
From 4e3442155c0a7f13062be1d64be540e6a3aec1f4 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sat, 3 Oct 2026 13:57:33 +0000
Subject: [PATCH 11/12] Strings.CS.removeStart; argumentSet display names;
merge fall-back tests
---
.../processor/analysis/JavaLangMapper.java | 4 +-
.../processor/BuilderScopeResolverTest.java | 281 ++++++++----------
2 files changed, 131 insertions(+), 154 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
index db6d9b24..5d4c53ff 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
@@ -39,7 +39,7 @@
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.SimpleTypeVisitor14;
-import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Strings;
import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto;
import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto;
import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto;
@@ -143,7 +143,7 @@ public static TypeName mapToTypeName(TypeElement typeElement, ProcessingContext
}
String packageName = context.getPackageName(typeElement);
String qualifiedName = typeElement.getQualifiedName().toString();
- String className = StringUtils.removeStart(qualifiedName, packageName + ".");
+ String className = Strings.CS.removeStart(qualifiedName, packageName + ".");
return new TypeName(packageName, className);
}
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 165e3fdc..c3011b16 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
@@ -40,6 +40,7 @@
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
+import javax.tools.JavaFileObject;
import org.javahelpers.simple.builders.core.enums.OptionState;
import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
@@ -147,10 +148,9 @@ public class LibHelper { public LibHelper() {} }
public class LibHelper { public LibHelper() {} }
""";
- @ParameterizedTest(name = "{0}")
+ @ParameterizedTest(name = "{argumentSetName}")
@MethodSource("resolvedContractVariants")
void resolverUsageScope_ResolvesContractVariants(
- String name,
String builderSource,
Supplier> probeResult,
String expectedBuilder,
@@ -174,7 +174,7 @@ void resolverUsageScope_ResolvesContractVariants(
static Stream resolvedContractVariants() {
return Stream.of(
// Usage scope without @SimpleBuilder annotation — builder resolved by contract check
- Arguments.of(
+ Arguments.argumentSet(
"BuilderWithoutSimpleBuilderAnnotation",
"""
package lib;
@@ -192,7 +192,7 @@ public LibHelperBuilder(LibHelper value) {}
null),
// No accessible constructors: both instantiation paths come from the static
// factories — the second empty factory exercises the preferred-name ranking
- Arguments.of(
+ Arguments.argumentSet(
"StaticFactories",
"""
package lib;
@@ -212,7 +212,7 @@ private LibHelperBuilder() {}
"of"),
// Package-private contract members are accessible when the generated builder is in
// the same package (builderPackage "lib")
- Arguments.of(
+ Arguments.argumentSet(
"PackagePrivateMembers_SamePackage",
"""
package lib;
@@ -230,10 +230,10 @@ public class LibHelperBuilder {
null));
}
- @ParameterizedTest(name = "{0}")
+ @ParameterizedTest(name = "{argumentSetName}")
@MethodSource("rejectedContractVariants")
void resolverUsageScope_RejectsContractVariants(
- String name, String builderSource, Supplier> probeResult) {
+ String builderSource, Supplier> probeResult) {
assertThat(compileWithBuilder(builderSource)).succeeded();
assertEquals(Optional.empty(), probeResult.get());
}
@@ -242,7 +242,7 @@ static Stream rejectedContractVariants() {
return Stream.of(
// ctor(T) + build() but no no-arg ctor: generated consumer code calls `new
// LibHelperBuilder()`, so the builder must not qualify
- Arguments.of(
+ Arguments.argumentSet(
"BuilderWithoutNoArgConstructor",
"""
package lib;
@@ -253,7 +253,7 @@ public LibHelperBuilder(LibHelper value) {}
""",
probe(() -> ResolverProbeProcessor.usageWithoutAnnotation)),
// The no-arg ctor exists but is private: generated code could not call it
- Arguments.of(
+ Arguments.argumentSet(
"BuilderWithPrivateConstructor",
"""
package lib;
@@ -266,7 +266,7 @@ public LibHelperBuilder(LibHelper value) {}
probe(() -> ResolverProbeProcessor.usageWithoutAnnotation)),
// Package-private contract members are not accessible from a different package
// (builderPackage unset)
- Arguments.of(
+ Arguments.argumentSet(
"PackagePrivateMembers_OtherPackage",
"""
package lib;
@@ -280,10 +280,14 @@ public class LibHelperBuilder {
}
private static Compilation compileWithBuilder(String builderSource) {
+ return compileWithSources(LIB_HELPER, builderSource);
+ }
+
+ private static Compilation compileWithSources(String... sources) {
return Compiler.javac()
.withProcessors(new ResolverProbeProcessor())
.compile(
- ProcessorTestUtils.forSource(LIB_HELPER), ProcessorTestUtils.forSource(builderSource));
+ Stream.of(sources).map(ProcessorTestUtils::forSource).toArray(JavaFileObject[]::new));
}
private static Supplier> probe(
@@ -398,34 +402,6 @@ public LibHelperBuilder(lib.LibHelper value) {}
BuilderInstantiation.ConstructorCall.class, resolved.funcForPrefilledBuilder());
}
- @Test
- void resolverUsageScope_IgnoresInTypeAnchorWithoutContract() {
- Compilation compilation =
- Compiler.javac()
- .withProcessors(new ResolverProbeProcessor())
- .compile(
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class LibHelper {
- public static builders.NoContract builder() {
- return new builders.NoContract();
- }
- }
- """),
- ProcessorTestUtils.forSource(
- """
- package builders;
- public class NoContract {
- public NoContract() {}
- }
- """));
-
- assertThat(compilation).succeeded();
- // The anchored type does not satisfy the contract, so resolution falls through
- assertEquals(Optional.empty(), ResolverProbeProcessor.usageWithoutAnnotation);
- }
-
@Test
void resolverUsageScope_PrefersBuilderNamedInTypeFactory() {
Compilation compilation =
@@ -536,6 +512,120 @@ public static class LibHelperBuilder {
assertEquals("toBuilder", prefilled.methodName());
}
+ /** Fixtures where no contract-satisfying builder is reachable from the referenced type. */
+ @ParameterizedTest(name = "{argumentSetName}")
+ @MethodSource("fallBackVariants")
+ void resolverUsageScope_FallsBackToClasspathCandidate(String[] sources) {
+ assertThat(compileWithSources(sources)).succeeded();
+ assertEquals(Optional.empty(), ResolverProbeProcessor.usageWithoutAnnotation);
+ }
+
+ static Stream fallBackVariants() {
+ return Stream.of(
+ // The anchored type does not satisfy the contract, so resolution falls through
+ Arguments.argumentSet(
+ "InTypeAnchorWithoutContract",
+ (Object)
+ new String[] {
+ """
+ package lib;
+ public class LibHelper {
+ public static builders.NoContract builder() {
+ return new builders.NoContract();
+ }
+ }
+ """,
+ """
+ package builders;
+ public class NoContract {
+ public NoContract() {}
+ }
+ """
+ }),
+ // Delombok output of @Builder without toBuilder: builder() anchors the empty path,
+ // but no seeded path exists, so the anchored candidate is rejected
+ Arguments.argumentSet(
+ "LombokBuilderShapeWithoutToBuilder",
+ (Object)
+ new String[] {
+ """
+ package lib;
+ public class LibHelper {
+ public static LibHelperBuilder builder() { return new LibHelperBuilder(); }
+ public static class LibHelperBuilder {
+ LibHelperBuilder() {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ }
+ """
+ }),
+ // org.inferred.freebuilder generated shape: nested Builder with a public ctor and
+ // mergeFrom(T) — mergeFrom is an instance method, so the contract's seeded path fails
+ Arguments.argumentSet(
+ "FreeBuilderShape",
+ (Object)
+ new String[] {
+ """
+ package lib;
+ public class LibHelper {
+ public static class Builder {
+ public Builder() {}
+ public Builder mergeFrom(LibHelper value) { return this; }
+ public LibHelper build() { return new LibHelper(); }
+ }
+ }
+ """
+ }),
+ // com.google.auto.value generated shape: create() anchors a builder on the generated
+ // sibling, but AutoValue offers no seeded path (no ctor(T), no toBuilder)
+ Arguments.argumentSet(
+ "AutoValueShape",
+ (Object)
+ new String[] {
+ """
+ package lib;
+ public class LibHelper {
+ public static AutoValue_LibHelper.Builder create() {
+ return new AutoValue_LibHelper.Builder();
+ }
+ }
+ """,
+ """
+ package lib;
+ public class AutoValue_LibHelper {
+ public static class Builder {
+ Builder() {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ }
+ """
+ }),
+ // org.immutables generated shape: builder() and toBuilder() live on the generated
+ // sibling ImmutableLibHelper, unreachable from the referenced type itself
+ Arguments.argumentSet(
+ "ImmutablesShape",
+ (Object)
+ new String[] {
+ """
+ package lib;
+ public class LibHelper {
+ public LibHelper() {}
+ }
+ """,
+ """
+ package lib;
+ public class ImmutableLibHelper {
+ public static Builder builder() { return new Builder(); }
+ public Builder toBuilder() { return new Builder(); }
+ public static class Builder {
+ public Builder() {}
+ public ImmutableLibHelper build() { return new ImmutableLibHelper(); }
+ }
+ }
+ """
+ }));
+ }
+
@Test
void resolverUsageScope_ResolvesNestedTypeBuilder() {
Compilation compilation =
@@ -598,119 +688,6 @@ public static class LibHelperBuilder {
assertEquals("toBuilder", prefilled.methodName());
}
- @Test
- void resolverUsageScope_LombokBuilderShapeWithoutToBuilderFallsBack() {
- Compilation compilation =
- Compiler.javac()
- .withProcessors(new ResolverProbeProcessor())
- .compile(
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class LibHelper {
- public static LibHelperBuilder builder() { return new LibHelperBuilder(); }
- public static class LibHelperBuilder {
- LibHelperBuilder() {}
- public LibHelper build() { return new LibHelper(); }
- }
- }
- """));
-
- assertThat(compilation).succeeded();
- // Delombok output of @Builder without toBuilder: builder() anchors the empty path, but
- // no seeded path exists, so the anchored candidate is rejected
- assertEquals(Optional.empty(), ResolverProbeProcessor.usageWithoutAnnotation);
- }
-
- @Test
- void resolverUsageScope_FreeBuilderShapeFallsBack() {
- Compilation compilation =
- Compiler.javac()
- .withProcessors(new ResolverProbeProcessor())
- .compile(
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class LibHelper {
- public static class Builder {
- public Builder() {}
- public Builder mergeFrom(LibHelper value) { return this; }
- public LibHelper build() { return new LibHelper(); }
- }
- }
- """));
-
- assertThat(compilation).succeeded();
- // org.inferred.freebuilder generated shape: nested Builder with a public ctor and
- // mergeFrom(T) — mergeFrom is an instance method, so the contract's seeded path fails
- // and resolution falls back to the classpath candidate (absent here)
- assertEquals(Optional.empty(), ResolverProbeProcessor.usageWithoutAnnotation);
- }
-
- @Test
- void resolverUsageScope_AutoValueShapeFallsBack() {
- Compilation compilation =
- Compiler.javac()
- .withProcessors(new ResolverProbeProcessor())
- .compile(
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class LibHelper {
- public static AutoValue_LibHelper.Builder create() {
- return new AutoValue_LibHelper.Builder();
- }
- }
- """),
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class AutoValue_LibHelper {
- public static class Builder {
- Builder() {}
- public LibHelper build() { return new LibHelper(); }
- }
- }
- """));
-
- assertThat(compilation).succeeded();
- // com.google.auto.value generated shape: create() anchors a builder on the generated
- // sibling, but AutoValue offers no seeded path (no ctor(T), no toBuilder)
- assertEquals(Optional.empty(), ResolverProbeProcessor.usageWithoutAnnotation);
- }
-
- @Test
- void resolverUsageScope_ImmutablesShapeFallsBack() {
- Compilation compilation =
- Compiler.javac()
- .withProcessors(new ResolverProbeProcessor())
- .compile(
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class LibHelper {
- public LibHelper() {}
- }
- """),
- ProcessorTestUtils.forSource(
- """
- package lib;
- public class ImmutableLibHelper {
- public static Builder builder() { return new Builder(); }
- public Builder toBuilder() { return new Builder(); }
- public static class Builder {
- public Builder() {}
- public ImmutableLibHelper build() { return new ImmutableLibHelper(); }
- }
- }
- """));
-
- assertThat(compilation).succeeded();
- // org.immutables generated shape: builder() and toBuilder() live on the generated
- // sibling ImmutableLibHelper, unreachable from the referenced type itself
- assertEquals(Optional.empty(), ResolverProbeProcessor.usageWithoutAnnotation);
- }
-
@Test
void resolverUsageScope_ResolvesRecordBuilderShape() {
Compilation compilation =
From a4008bbee7b043c9300aaa217f728d7b58ce7db9 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sat, 3 Oct 2026 14:27:40 +0000
Subject: [PATCH 12/12] Collect test helpers at the bottom of
BuilderScopeResolverTest
---
.../processor/BuilderScopeResolverTest.java | 477 +++++++++---------
1 file changed, 239 insertions(+), 238 deletions(-)
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 c3011b16..a6664bd9 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
@@ -142,12 +142,6 @@ public class LibHelper { public LibHelper() {} }
ResolverProbeProcessor.usageAfterRegistration.get().typeName().getFullQualifiedName());
}
- private static final String LIB_HELPER =
- """
- package lib;
- public class LibHelper { public LibHelper() {} }
- """;
-
@ParameterizedTest(name = "{argumentSetName}")
@MethodSource("resolvedContractVariants")
void resolverUsageScope_ResolvesContractVariants(
@@ -171,65 +165,6 @@ void resolverUsageScope_ResolvesContractVariants(
assertFactoryMethod(expectedPrefilledMethod, prefilled);
}
- static Stream resolvedContractVariants() {
- return Stream.of(
- // Usage scope without @SimpleBuilder annotation — builder resolved by contract check
- Arguments.argumentSet(
- "BuilderWithoutSimpleBuilderAnnotation",
- """
- package lib;
- public class LibHelperBuilder {
- public LibHelperBuilder() {}
- public LibHelperBuilder(LibHelper value) {}
- public LibHelper build() { return new LibHelper(); }
- }
- """,
- probe(() -> ResolverProbeProcessor.usageWithoutAnnotation),
- "lib.LibHelperBuilder",
- BuilderInstantiation.ConstructorCall.class,
- null,
- BuilderInstantiation.ConstructorCall.class,
- null),
- // No accessible constructors: both instantiation paths come from the static
- // factories — the second empty factory exercises the preferred-name ranking
- Arguments.argumentSet(
- "StaticFactories",
- """
- package lib;
- public class LibHelperBuilder {
- private LibHelperBuilder() {}
- public static LibHelperBuilder create() { return new LibHelperBuilder(); }
- public static LibHelperBuilder make() { return create(); }
- public static LibHelperBuilder of(LibHelper value) { return create(); }
- public LibHelper build() { return new LibHelper(); }
- }
- """,
- probe(() -> ResolverProbeProcessor.usageWithoutAnnotation),
- "lib.LibHelperBuilder",
- BuilderInstantiation.StaticFactoryCall.class,
- "create",
- BuilderInstantiation.StaticFactoryCall.class,
- "of"),
- // Package-private contract members are accessible when the generated builder is in
- // the same package (builderPackage "lib")
- Arguments.argumentSet(
- "PackagePrivateMembers_SamePackage",
- """
- package lib;
- public class LibHelperBuilder {
- LibHelperBuilder() {}
- LibHelperBuilder(LibHelper value) {}
- LibHelper build() { return new LibHelper(); }
- }
- """,
- probe(() -> ResolverProbeProcessor.usagePackagePrivate),
- "lib.LibHelperBuilder",
- BuilderInstantiation.ConstructorCall.class,
- null,
- BuilderInstantiation.ConstructorCall.class,
- null));
- }
-
@ParameterizedTest(name = "{argumentSetName}")
@MethodSource("rejectedContractVariants")
void resolverUsageScope_RejectsContractVariants(
@@ -238,73 +173,6 @@ void resolverUsageScope_RejectsContractVariants(
assertEquals(Optional.empty(), probeResult.get());
}
- static Stream rejectedContractVariants() {
- return Stream.of(
- // ctor(T) + build() but no no-arg ctor: generated consumer code calls `new
- // LibHelperBuilder()`, so the builder must not qualify
- Arguments.argumentSet(
- "BuilderWithoutNoArgConstructor",
- """
- package lib;
- public class LibHelperBuilder {
- public LibHelperBuilder(LibHelper value) {}
- public LibHelper build() { return new LibHelper(); }
- }
- """,
- probe(() -> ResolverProbeProcessor.usageWithoutAnnotation)),
- // The no-arg ctor exists but is private: generated code could not call it
- Arguments.argumentSet(
- "BuilderWithPrivateConstructor",
- """
- package lib;
- public class LibHelperBuilder {
- private LibHelperBuilder() {}
- public LibHelperBuilder(LibHelper value) {}
- public LibHelper build() { return new LibHelper(); }
- }
- """,
- probe(() -> ResolverProbeProcessor.usageWithoutAnnotation)),
- // Package-private contract members are not accessible from a different package
- // (builderPackage unset)
- Arguments.argumentSet(
- "PackagePrivateMembers_OtherPackage",
- """
- package lib;
- public class LibHelperBuilder {
- LibHelperBuilder() {}
- LibHelperBuilder(LibHelper value) {}
- LibHelper build() { return new LibHelper(); }
- }
- """,
- probe(() -> ResolverProbeProcessor.usageWithoutAnnotation)));
- }
-
- private static Compilation compileWithBuilder(String builderSource) {
- return compileWithSources(LIB_HELPER, builderSource);
- }
-
- private static Compilation compileWithSources(String... sources) {
- return Compiler.javac()
- .withProcessors(new ResolverProbeProcessor())
- .compile(
- Stream.of(sources).map(ProcessorTestUtils::forSource).toArray(JavaFileObject[]::new));
- }
-
- private static Supplier> probe(
- Supplier> field) {
- return field;
- }
-
- private static void assertFactoryMethod(
- String expectedMethod, BuilderInstantiation instantiation) {
- if (expectedMethod != null) {
- assertEquals(
- expectedMethod,
- assertInstanceOf(BuilderInstantiation.StaticFactoryCall.class, instantiation)
- .methodName());
- }
- }
-
@Test
void resolverUsageScope_UsesBuilderUsageSuffixWhenConfigured() {
Compilation compilation =
@@ -520,112 +388,6 @@ void resolverUsageScope_FallsBackToClasspathCandidate(String[] sources) {
assertEquals(Optional.empty(), ResolverProbeProcessor.usageWithoutAnnotation);
}
- static Stream fallBackVariants() {
- return Stream.of(
- // The anchored type does not satisfy the contract, so resolution falls through
- Arguments.argumentSet(
- "InTypeAnchorWithoutContract",
- (Object)
- new String[] {
- """
- package lib;
- public class LibHelper {
- public static builders.NoContract builder() {
- return new builders.NoContract();
- }
- }
- """,
- """
- package builders;
- public class NoContract {
- public NoContract() {}
- }
- """
- }),
- // Delombok output of @Builder without toBuilder: builder() anchors the empty path,
- // but no seeded path exists, so the anchored candidate is rejected
- Arguments.argumentSet(
- "LombokBuilderShapeWithoutToBuilder",
- (Object)
- new String[] {
- """
- package lib;
- public class LibHelper {
- public static LibHelperBuilder builder() { return new LibHelperBuilder(); }
- public static class LibHelperBuilder {
- LibHelperBuilder() {}
- public LibHelper build() { return new LibHelper(); }
- }
- }
- """
- }),
- // org.inferred.freebuilder generated shape: nested Builder with a public ctor and
- // mergeFrom(T) — mergeFrom is an instance method, so the contract's seeded path fails
- Arguments.argumentSet(
- "FreeBuilderShape",
- (Object)
- new String[] {
- """
- package lib;
- public class LibHelper {
- public static class Builder {
- public Builder() {}
- public Builder mergeFrom(LibHelper value) { return this; }
- public LibHelper build() { return new LibHelper(); }
- }
- }
- """
- }),
- // com.google.auto.value generated shape: create() anchors a builder on the generated
- // sibling, but AutoValue offers no seeded path (no ctor(T), no toBuilder)
- Arguments.argumentSet(
- "AutoValueShape",
- (Object)
- new String[] {
- """
- package lib;
- public class LibHelper {
- public static AutoValue_LibHelper.Builder create() {
- return new AutoValue_LibHelper.Builder();
- }
- }
- """,
- """
- package lib;
- public class AutoValue_LibHelper {
- public static class Builder {
- Builder() {}
- public LibHelper build() { return new LibHelper(); }
- }
- }
- """
- }),
- // org.immutables generated shape: builder() and toBuilder() live on the generated
- // sibling ImmutableLibHelper, unreachable from the referenced type itself
- Arguments.argumentSet(
- "ImmutablesShape",
- (Object)
- new String[] {
- """
- package lib;
- public class LibHelper {
- public LibHelper() {}
- }
- """,
- """
- package lib;
- public class ImmutableLibHelper {
- public static Builder builder() { return new Builder(); }
- public Builder toBuilder() { return new Builder(); }
- public static class Builder {
- public Builder() {}
- public ImmutableLibHelper build() { return new ImmutableLibHelper(); }
- }
- }
- """
- }));
- }
-
@Test
void resolverUsageScope_ResolvesNestedTypeBuilder() {
Compilation compilation =
@@ -755,6 +517,245 @@ public LibHelperBuilder(LibHelper value) {}
BuilderInstantiation.ConstructorCall.class, generated.funcForPrefilledBuilder());
}
+ private static final String LIB_HELPER =
+ """
+ package lib;
+ public class LibHelper { public LibHelper() {} }
+ """;
+
+ static Stream resolvedContractVariants() {
+ return Stream.of(
+ // Usage scope without @SimpleBuilder annotation — builder resolved by contract check
+ Arguments.argumentSet(
+ "BuilderWithoutSimpleBuilderAnnotation",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ public LibHelperBuilder() {}
+ public LibHelperBuilder(LibHelper value) {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usageWithoutAnnotation),
+ "lib.LibHelperBuilder",
+ BuilderInstantiation.ConstructorCall.class,
+ null,
+ BuilderInstantiation.ConstructorCall.class,
+ null),
+ // No accessible constructors: both instantiation paths come from the static
+ // factories — the second empty factory exercises the preferred-name ranking
+ Arguments.argumentSet(
+ "StaticFactories",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ private LibHelperBuilder() {}
+ public static LibHelperBuilder create() { return new LibHelperBuilder(); }
+ public static LibHelperBuilder make() { return create(); }
+ public static LibHelperBuilder of(LibHelper value) { return create(); }
+ public LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usageWithoutAnnotation),
+ "lib.LibHelperBuilder",
+ BuilderInstantiation.StaticFactoryCall.class,
+ "create",
+ BuilderInstantiation.StaticFactoryCall.class,
+ "of"),
+ // Package-private contract members are accessible when the generated builder is in
+ // the same package (builderPackage "lib")
+ Arguments.argumentSet(
+ "PackagePrivateMembers_SamePackage",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ LibHelperBuilder() {}
+ LibHelperBuilder(LibHelper value) {}
+ LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usagePackagePrivate),
+ "lib.LibHelperBuilder",
+ BuilderInstantiation.ConstructorCall.class,
+ null,
+ BuilderInstantiation.ConstructorCall.class,
+ null));
+ }
+
+ static Stream rejectedContractVariants() {
+ return Stream.of(
+ // ctor(T) + build() but no no-arg ctor: generated consumer code calls `new
+ // LibHelperBuilder()`, so the builder must not qualify
+ Arguments.argumentSet(
+ "BuilderWithoutNoArgConstructor",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ public LibHelperBuilder(LibHelper value) {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usageWithoutAnnotation)),
+ // The no-arg ctor exists but is private: generated code could not call it
+ Arguments.argumentSet(
+ "BuilderWithPrivateConstructor",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ private LibHelperBuilder() {}
+ public LibHelperBuilder(LibHelper value) {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usageWithoutAnnotation)),
+ // Package-private contract members are not accessible from a different package
+ // (builderPackage unset)
+ Arguments.argumentSet(
+ "PackagePrivateMembers_OtherPackage",
+ """
+ package lib;
+ public class LibHelperBuilder {
+ LibHelperBuilder() {}
+ LibHelperBuilder(LibHelper value) {}
+ LibHelper build() { return new LibHelper(); }
+ }
+ """,
+ probe(() -> ResolverProbeProcessor.usageWithoutAnnotation)));
+ }
+
+ static Stream fallBackVariants() {
+ return Stream.of(
+ // The anchored type does not satisfy the contract, so resolution falls through
+ Arguments.argumentSet(
+ "InTypeAnchorWithoutContract",
+ (Object)
+ new String[] {
+ """
+ package lib;
+ public class LibHelper {
+ public static builders.NoContract builder() {
+ return new builders.NoContract();
+ }
+ }
+ """,
+ """
+ package builders;
+ public class NoContract {
+ public NoContract() {}
+ }
+ """
+ }),
+ // Delombok output of @Builder without toBuilder: builder() anchors the empty path,
+ // but no seeded path exists, so the anchored candidate is rejected
+ Arguments.argumentSet(
+ "LombokBuilderShapeWithoutToBuilder",
+ (Object)
+ new String[] {
+ """
+ package lib;
+ public class LibHelper {
+ public static LibHelperBuilder builder() { return new LibHelperBuilder(); }
+ public static class LibHelperBuilder {
+ LibHelperBuilder() {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ }
+ """
+ }),
+ // org.inferred.freebuilder generated shape: nested Builder with a public ctor and
+ // mergeFrom(T) — mergeFrom is an instance method, so the contract's seeded path fails
+ Arguments.argumentSet(
+ "FreeBuilderShape",
+ (Object)
+ new String[] {
+ """
+ package lib;
+ public class LibHelper {
+ public static class Builder {
+ public Builder() {}
+ public Builder mergeFrom(LibHelper value) { return this; }
+ public LibHelper build() { return new LibHelper(); }
+ }
+ }
+ """
+ }),
+ // com.google.auto.value generated shape: create() anchors a builder on the generated
+ // sibling, but AutoValue offers no seeded path (no ctor(T), no toBuilder)
+ Arguments.argumentSet(
+ "AutoValueShape",
+ (Object)
+ new String[] {
+ """
+ package lib;
+ public class LibHelper {
+ public static AutoValue_LibHelper.Builder create() {
+ return new AutoValue_LibHelper.Builder();
+ }
+ }
+ """,
+ """
+ package lib;
+ public class AutoValue_LibHelper {
+ public static class Builder {
+ Builder() {}
+ public LibHelper build() { return new LibHelper(); }
+ }
+ }
+ """
+ }),
+ // org.immutables generated shape: builder() and toBuilder() live on the generated
+ // sibling ImmutableLibHelper, unreachable from the referenced type itself
+ Arguments.argumentSet(
+ "ImmutablesShape",
+ (Object)
+ new String[] {
+ """
+ package lib;
+ public class LibHelper {
+ public LibHelper() {}
+ }
+ """,
+ """
+ package lib;
+ public class ImmutableLibHelper {
+ public static Builder builder() { return new Builder(); }
+ public Builder toBuilder() { return new Builder(); }
+ public static class Builder {
+ public Builder() {}
+ public ImmutableLibHelper build() { return new ImmutableLibHelper(); }
+ }
+ }
+ """
+ }));
+ }
+
+ private static Compilation compileWithSources(String... sources) {
+ return Compiler.javac()
+ .withProcessors(new ResolverProbeProcessor())
+ .compile(
+ Stream.of(sources).map(ProcessorTestUtils::forSource).toArray(JavaFileObject[]::new));
+ }
+
+ private static Compilation compileWithBuilder(String builderSource) {
+ return compileWithSources(LIB_HELPER, builderSource);
+ }
+
+
+ private static Supplier> probe(
+ Supplier> field) {
+ return field;
+ }
+
+ private static void assertFactoryMethod(
+ String expectedMethod, BuilderInstantiation instantiation) {
+ if (expectedMethod != null) {
+ assertEquals(
+ expectedMethod,
+ assertInstanceOf(BuilderInstantiation.StaticFactoryCall.class, instantiation)
+ .methodName());
+ }
+ }
+
private static final class ResolverProbeProcessor extends AbstractProcessor {
private static Optional first;
private static Optional second;