Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,12 @@ public BuilderScopeResolver(ProcessingContext context) {
* the classpath and returned if it satisfies the builder contract: an instantiation path
* for an empty builder (no-arg constructor or static factory like {@code create()}), an
* instantiation path seeded with the value (constructor accepting the referenced type or
* static factory like {@code create(T)}/{@code of(T)}), and a no-arg {@code build()} method
* returning it - each accessible from the generated builder's package. 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}.
* static factory like {@code create(T)}/{@code of(T)}), and an accessible, parameterless
* method returning it (the build method, regardless of name) - each accessible from the
* generated builder's package. 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 @@ -248,12 +249,12 @@ private Optional<ResolvedBuilder> resolve(TypeElement referencedType) {
* Looks up the candidate builder type on the classpath and verifies it satisfies the builder
* contract: a way to create an empty instance (a no-arg constructor or a static parameterless
* factory like {@code create()}), a way to create an instance seeded with a value (a constructor
* accepting the referenced type or a static factory like {@code create(T)}/{@code of(T)}), and a
* no-arg {@code build()} method returning it - each accessible from the generated builder's
* package, since the generated code calls them from there. 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}.
* accepting the referenced type or a static factory like {@code create(T)}/{@code of(T)}), and an
* accessible, parameterless method returning it (the build method, regardless of name) - each
* accessible from the generated builder's package, since the generated code calls them from
* there. 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 referenced type the builder must accept and return
Expand All @@ -270,13 +271,18 @@ private Optional<ResolvedBuilder> resolveByBuilderContract(
resolveFuncForEmptyBuilder(builderTypeElement);
Optional<BuilderInstantiation> funcForPrefilledBuilder =
resolveFuncForPrefilledBuilder(builderTypeElement, expectedType);
Optional<ExecutableElement> buildMethod = findBuildMethod(builderTypeElement, expectedType);
if (funcForEmptyBuilder.isEmpty()
|| funcForPrefilledBuilder.isEmpty()
|| !JavaLangAnalyser.hasBuildMethodReturning(builderTypeElement, expectedType, context)) {
|| buildMethod.isEmpty()) {
return Optional.empty();
}
return Optional.of(
new ResolvedBuilder(candidate, funcForEmptyBuilder.get(), funcForPrefilledBuilder.get()));
new ResolvedBuilder(
candidate,
funcForEmptyBuilder.get(),
funcForPrefilledBuilder.get(),
buildMethod.get().getSimpleName().toString()));
}

/**
Expand Down Expand Up @@ -397,9 +403,8 @@ private Optional<ResolvedBuilder> resolveAnchoredBuilder(
for (ExecutableElement factory : factories) {
TypeElement builderElement =
(TypeElement) ((DeclaredType) factory.getReturnType()).asElement();
if (!context.isMemberAccessibleFromBuilderPackage(builderElement)
|| !JavaLangAnalyser.hasBuildMethodReturning(
builderElement, referencedTypeName, context)) {
Optional<ExecutableElement> buildMethod = findBuildMethod(builderElement, referencedTypeName);
if (!context.isMemberAccessibleFromBuilderPackage(builderElement) || buildMethod.isEmpty()) {
continue;
}
Optional<BuilderInstantiation> funcForPrefilledBuilder =
Expand All @@ -410,7 +415,8 @@ private Optional<ResolvedBuilder> resolveAnchoredBuilder(
JavaLangMapper.mapToTypeName(builderElement, context),
new BuilderInstantiation.AnchorFactoryCall(
referencedTypeName, factory.getSimpleName().toString()),
funcForPrefilledBuilder.get()));
funcForPrefilledBuilder.get(),
buildMethod.get().getSimpleName().toString()));
}
}
return Optional.empty();
Expand Down Expand Up @@ -491,6 +497,31 @@ private static int preferredFactoryNameRank(String name) {
return index < 0 ? PREFERRED_FACTORY_NAMES.size() : index;
}

/**
* Finds the build method on the builder type: an accessible, non-static, parameterless method
* returning the referenced type, regardless of its name. When several candidates exist, {@code
* build} is preferred, then the alphabetically first name. Methods whose signature matches a
* {@link java.lang.Object} method (e.g. {@code toString()}) are excluded so types like {@code
* StringBuilder} do not satisfy the contract for {@code String}.
*
* @param builderType the candidate builder type to inspect
* @param expectedType the referenced type the build method must return
* @return the selected build method, or empty if no matching method exists
*/
private Optional<ExecutableElement> findBuildMethod(
TypeElement builderType, TypeName expectedType) {
return JavaLangAnalyser.findMethods(builderType, List.of(), expectedType, context).stream()
.filter(
method ->
JavaLangAnalyser.isNotStatic(method)
&& JavaLangAnalyser.isNoMethodOfObjectClass(method, context))
.min(
Comparator.comparingInt(
(ExecutableElement method) ->
"build".contentEquals(method.getSimpleName()) ? 0 : 1)
.thenComparing(method -> method.getSimpleName().toString()));
}

private void refreshForConfigurationIfNeeded() {
BuilderConfiguration configuration = context.getConfiguration();
ResolutionInputs inputs = new ResolutionInputs(configuration, context.getBuilderPackageName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,23 @@ public final class JavaLangAnalyser {
private JavaLangAnalyser() {}

/**
* Helper function to filter methods from {@code java.lang.Object}.
* Helper function to filter methods from {@code java.lang.Object}. The comparison is
* signature-based, so overrides like {@code StringBuilder#toString} count as Object methods too.
*
* @param mth ExecutableElement to be validated
* @return {@code true}, if it is no method from java.lang.Object
* @param context the processing context, used to resolve {@link java.lang.Object}'s methods
* @return {@code true}, if it is no method with the signature of a java.lang.Object method
*/
public static boolean isNoMethodOfObjectClass(ExecutableElement mth) {
String simpleNameOfParent = mth.getEnclosingElement().getSimpleName().toString();
return !(Strings.CS.equals("java.lang.Object", simpleNameOfParent)
|| Strings.CS.equals("Object", simpleNameOfParent));
public static boolean isNoMethodOfObjectClass(ExecutableElement mth, ProcessingContext context) {
TypeElement objectElement = context.getTypeElement(Object.class.getCanonicalName());
if (objectElement == null) {
return true;
}
return ElementFilter.methodsIn(objectElement.getEnclosedElements()).stream()
.noneMatch(
objectMethod ->
objectMethod.getSimpleName().equals(mth.getSimpleName())
&& objectMethod.getParameters().size() == mth.getParameters().size());
}

/**
Expand All @@ -80,7 +88,7 @@ public static boolean isNoMethodOfObjectClass(ExecutableElement mth) {
public static List<ExecutableElement> findAllPossibleSettersOfClass(
TypeElement typeElement, ProcessingContext context) {
return ElementFilter.methodsIn(context.getAllMembers(typeElement)).stream()
.filter(JavaLangAnalyser::isNoMethodOfObjectClass)
.filter(mth -> isNoMethodOfObjectClass(mth, context))
.filter(JavaLangAnalyser::isSetterForField)
.sorted(
(m1, m2) -> {
Expand Down Expand Up @@ -243,24 +251,6 @@ public static Optional<AnnotationValue> findAnnotationAttribute(
return Optional.empty();
}

/**
* Checks whether the given type has a no-arg {@code build()} method returning the expected type.
* This is part of the builder contract used when the built value is retrieved.
*
* <p>The check is annotation-agnostic and avoids false positives like {@code StringBuilder} for
* {@code String}.
*
* @param builderType the candidate builder type element to check
* @param expectedReturnType the type that {@code build()} must return
* @param context the processing context, used to access all members
* @return {@code true} if the type declares or inherits a matching {@code build()} method
*/
public static boolean hasBuildMethodReturning(
TypeElement builderType, TypeName expectedReturnType, ProcessingContext context) {
return findMethods(builderType, List.of(), expectedReturnType, context).stream()
.anyMatch(method -> method.getSimpleName().contentEquals("build"));
}

/**
* Single-parameter variant of {@link #findMethodsStatic(TypeElement, List, TypeName,
* ProcessingContext)}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@ public static BuilderMethodDto createFieldConsumerWithBuilder(
createBuilderMethod(field.getOriginalFieldName(), parentBuilderType, context);
methodDto.addParameter(parameter);

String buildExpression = calculateBuildExpression(field.getFieldType());
String buildExpression =
wrapConcreteCollectionType(field.getFieldType(), fieldBuilder.buildCall("builder"));

methodDto.setCode(
"""
Expand Down Expand Up @@ -281,16 +282,6 @@ public static TypeNameGeneric createConsumerType(TypeName builderType) {
return new TypeNameGeneric(JavaLangMapper.map2TypeName(Consumer.class), builderType);
}

/**
* Calculates the build expression for a field type, wrapping concrete collections if needed.
*
* @param fieldType the field type
* @return the build expression
*/
private static String calculateBuildExpression(TypeName fieldType) {
return wrapConcreteCollectionType(fieldType, "builder.build()");
}

/**
* Wraps an expression with a concrete collection constructor if needed.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@
* {@code B.create()})
* @param funcForPrefilledBuilder how to obtain a builder instance seeded with a value (e.g. {@code
* new B(value)} or {@code B.of(value)})
* @param buildMethodName the name of the parameterless method returning the built value (e.g.
* {@code build})
*/
public record ResolvedBuilder(
TypeName typeName,
BuilderInstantiation funcForEmptyBuilder,
BuilderInstantiation funcForPrefilledBuilder) {
BuilderInstantiation funcForPrefilledBuilder,
String buildMethodName) {

/**
* A resolution instantiated exclusively through constructors.
* A resolution instantiated exclusively through constructors, building with {@code build()}.
*
* @param typeName the builder type to reference
*/
Expand All @@ -44,4 +47,30 @@ public ResolvedBuilder(TypeName typeName) {
new BuilderInstantiation.ConstructorCall(),
new BuilderInstantiation.ConstructorCall());
}

/**
* A resolution whose terminal build method is named {@code build}, as on builders generated by
* this processor.
*
* @param typeName the builder type to reference
* @param funcForEmptyBuilder how to obtain an empty builder instance
* @param funcForPrefilledBuilder how to obtain a builder instance seeded with a value
*/
public ResolvedBuilder(
TypeName typeName,
BuilderInstantiation funcForEmptyBuilder,
BuilderInstantiation funcForPrefilledBuilder) {
this(typeName, funcForEmptyBuilder, funcForPrefilledBuilder, "build");
}

/**
* Renders the terminal call producing the built value: {@code
* builderExpression.buildMethodName()}.
*
* @param builderExpression expression evaluating to the builder instance (e.g. {@code builder})
* @return the build call code
*/
public String buildCall(String builderExpression) {
return builderExpression + "." + buildMethodName + "()";
}
}
Loading
Loading