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
Original file line number Diff line number Diff line change
Expand Up @@ -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). <br>
* 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.
*
* <p>Default: ENABLED <br>
* 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. <br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
generateJavaDoc = OptionState.DISABLED,
copyTypeAnnotations = OptionState.DISABLED,
implementsBuilderBase = OptionState.DISABLED,
usingExistingBuilders = OptionState.DISABLED,
builderSuffix = "Builder",
setterSuffix = "",
formattingMode = "lightweight"))
Expand Down
23 changes: 23 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,16 @@ Controls which packages may provide builders as nested builder helpers. Packages
comma-separated, each listed package includes all of its subpackages, and matching ignores
case.

Before constructing a candidate name, the processor checks the referenced type
itself for an anchored builder — the type's own declaration wins over the
same-package candidate below. Accessible nested types satisfying the contract
are checked first (e.g. `Person.PersonBuilder` or `Person.Builder`), then a
static parameterless factory on the type — `builder()` preferred — whose return
type must be accessible and declare a no-arg `build()` method (Lombok/Immutables
style). The factory itself is the empty-instantiation path; the seeded path is
the builder's own contract path, a static `builder(T)`-style method, or an
instance method on the value like `toBuilder()`.

The processor constructs the candidate builder name using `builderUsageSuffix`
(or `builderSuffix` if not configured) and verifies the builder contract: a way
to create an empty builder (a no-arg constructor or a static parameterless
Expand All @@ -769,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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@
import java.util.Map;
import java.util.Optional;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.ElementFilter;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
import org.javahelpers.simple.builders.processor.model.core.PackageScopes;
Expand Down Expand Up @@ -96,6 +100,12 @@ public BuilderScopeResolver(ProcessingContext context) {
* <li>If the referenced type's builder is generated in the current processing round (registered
* via {@link #registerGeneratedBuilder}), the registered builder name is returned
* immediately — trusted without a classpath lookup or contract check.
* <li>Otherwise, a builder anchored inside the referenced type itself wins over the
* same-package candidate: first accessible nested types satisfying the builder contract
* (e.g. {@code Person.PersonBuilder}), then static parameterless factory methods on the
* type returning a builder type with a no-arg {@code build()} (e.g. {@code
* Person.builder()}), with a seeded path on the type ({@code builder(T)}/{@code
* toBuilder()}) or on the builder.
* <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: an instantiation path
Expand Down Expand Up @@ -210,6 +220,22 @@ private Optional<ResolvedBuilder> 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.
Optional<ResolvedBuilder> inTypeBuilder =
resolveInTypeBuilder(referencedType, referencedTypeName);
if (inTypeBuilder.isPresent()) {
return inTypeBuilder;
}

// For types not generated in this round, look up the candidate on the classpath using
// builderUsageSuffix (which falls back to builderSuffix if not configured) and verify
// the builder contract.
Expand Down Expand Up @@ -289,6 +315,152 @@ private Optional<BuilderInstantiation> resolveFuncForPrefilledBuilder(
return func;
}

/**
* Resolves a builder anchored inside the referenced type itself, like the nested {@code
* Person.PersonBuilder} or the static {@code Person.builder()} factory that Lombok, Immutables or
* FreeBuilder produce. Nested types are ordinary contract candidates checked through {@link
* #resolveByBuilderContract} first; then static parameterless factory methods on the type anchor
* the empty-instantiation path directly.
*
* @param referencedType the type element being referenced
* @param referencedTypeName the referenced type, passed to the contract check
* @return the resolved builder, or empty when the type anchors no usable builder
*/
private Optional<ResolvedBuilder> resolveInTypeBuilder(
TypeElement referencedType, TypeName referencedTypeName) {
Optional<ResolvedBuilder> nestedBuilder =
nestedBuilderCandidates(referencedType).stream()
.map(candidate -> resolveByBuilderContract(candidate, referencedTypeName))
.flatMap(Optional::stream)
.findFirst();
return nestedBuilder.or(() -> resolveAnchoredBuilder(referencedType, referencedTypeName));
}

/**
* Collects the accessible nested types of the referenced type as builder candidates, like the
* {@code Person.PersonBuilder} or {@code Person.Builder} inner classes Lombok and FreeBuilder
* generate. Names built from the referenced type's simple name and the builder suffix rank first,
* the plain builder suffix second; declaration order decides between equals.
*
* @param referencedType the type element being referenced
* @return the nested type candidates in preference order
*/
private List<TypeName> nestedBuilderCandidates(TypeElement referencedType) {
String builderSuffix = context.getConfiguration().getBuilderSuffix();
String preferredName = referencedType.getSimpleName() + builderSuffix;
return ElementFilter.typesIn(context.getAllMembers(referencedType)).stream()
.filter(context::isMemberAccessibleFromBuilderPackage)
.sorted(
Comparator.comparingInt(
nested -> nestedCandidateRank(nested, preferredName, builderSuffix)))
.map(nested -> JavaLangMapper.mapToTypeName(nested, context))
.toList();
}

/**
* Ranks a nested builder candidate by name: {@code <Simple><BuilderSuffix>} 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;
* the returned type must be accessible and declare a no-arg {@code build()} method returning the
* referenced type. The conventional {@code builder} method name wins over other candidate names;
* declaration order decides between equals.
*
* @param referencedType the type element being referenced
* @param referencedTypeName the referenced type, matched against {@code build()}'s return type
* @return the resolved builder, or empty when no factory anchors a usable builder
*/
private Optional<ResolvedBuilder> resolveAnchoredBuilder(
TypeElement referencedType, TypeName referencedTypeName) {
List<ExecutableElement> factories =
JavaLangAnalyser.findMethodsStatic(referencedType, List.of(), context).stream()
.filter(method -> method.getReturnType().getKind() == TypeKind.DECLARED)
.sorted(
Comparator.comparingInt(
method -> "builder".contentEquals(method.getSimpleName()) ? 0 : 1))
.toList();
for (ExecutableElement factory : factories) {
TypeElement builderElement =
(TypeElement) ((DeclaredType) factory.getReturnType()).asElement();
if (!context.isMemberAccessibleFromBuilderPackage(builderElement)
|| !JavaLangAnalyser.hasBuildMethodReturning(
builderElement, referencedTypeName, context)) {
continue;
}
Optional<BuilderInstantiation> funcForPrefilledBuilder =
resolveAnchoredPrefilledBuilder(referencedType, referencedTypeName, builderElement);
if (funcForPrefilledBuilder.isPresent()) {
return Optional.of(
new ResolvedBuilder(
JavaLangMapper.mapToTypeName(builderElement, context),
new BuilderInstantiation.AnchorFactoryCall(
referencedTypeName, factory.getSimpleName().toString()),
funcForPrefilledBuilder.get()));
}
}
return Optional.empty();
}

/**
* Resolves the seeded-instantiation path for an anchored builder: the builder's own contract path
* first, then a static factory on the referenced type accepting it (e.g. {@code
* Person.builder(person)}), then an instance method on the value (e.g. {@code
* person.toBuilder()}).
*
* @param referencedType the type element being referenced
* @param referencedTypeName the referenced type, passed to the contract check
* @param builderElement the anchored builder type
* @return the instantiation to emit, or empty when no seeded path exists
*/
private Optional<BuilderInstantiation> resolveAnchoredPrefilledBuilder(
TypeElement referencedType, TypeName referencedTypeName, TypeElement builderElement) {
Optional<BuilderInstantiation> func =
resolveFuncForPrefilledBuilder(builderElement, referencedTypeName);
if (func.isPresent()) {
return func;
}
TypeName builderTypeName = JavaLangMapper.mapToTypeName(builderElement, context);
func =
JavaLangAnalyser.findMethodsStatic(
referencedType, List.of(referencedTypeName), builderTypeName, context)
.stream()
.min(
Comparator.comparingInt(
method -> "builder".contentEquals(method.getSimpleName()) ? 0 : 1))
.map(
method ->
new BuilderInstantiation.AnchorFactoryCall(
referencedTypeName, method.getSimpleName().toString()));
if (func.isPresent()) {
return func;
}
return JavaLangAnalyser.findMethods(referencedType, List.of(), builderTypeName, context)
.stream()
.filter(JavaLangAnalyser::isNotStatic)
.min(
Comparator.comparingInt(
method -> "toBuilder".contentEquals(method.getSimpleName()) ? 0 : 1))
.map(
method ->
new BuilderInstantiation.ValueFactoryCall(
referencedTypeName, method.getSimpleName().toString()));
}

/**
* Finds an accessible static method on the builder type taking the expected parameter types and
* returning the builder type itself. When several candidates exist, {@code create} and {@code of}
Expand All @@ -305,9 +477,13 @@ private Optional<BuilderInstantiation> findStaticFactoryCall(
builderTypeElement, expectedParameterTypes, builderTypeName, context)
.stream()
.min(
Comparator.comparingInt(BuilderScopeResolver::preferredFactoryNameRank)
.thenComparing(Comparator.naturalOrder()))
.map(BuilderInstantiation.StaticFactoryCall::new);
Comparator.comparingInt(
(ExecutableElement method) ->
preferredFactoryNameRank(method.getSimpleName().toString()))
.thenComparing(method -> method.getSimpleName().toString()))
.map(
method ->
new BuilderInstantiation.StaticFactoryCall(method.getSimpleName().toString()));
}

private static int preferredFactoryNameRank(String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ public static boolean hasBuildMethodReturning(
* Single-parameter variant of {@link #findMethodsStatic(TypeElement, List, TypeName,
* ProcessingContext)}.
*/
public static List<String> findMethodsStatic(
public static List<ExecutableElement> findMethodsStatic(
TypeElement type,
TypeName expectedParameterType,
TypeName expectedReturnType,
Expand All @@ -281,34 +281,67 @@ public static List<String> findMethodsStatic(
* @param expectedParameterTypes the required parameter types
* @param expectedReturnType the required return type, or {@code null} for void-returning methods
* @param context the processing context, used to access all members
* @return the names of all matching methods, in declaration order
* @return the matching methods, in declaration order
*/
public static List<String> findMethodsStatic(
public static List<ExecutableElement> findMethodsStatic(
TypeElement type,
List<TypeName> expectedParameterTypes,
TypeName expectedReturnType,
ProcessingContext context) {
return findMethods(type, expectedParameterTypes, expectedReturnType, context).stream()
.filter(Predicate.not(JavaLangAnalyser::isNotStatic))
.map(method -> method.getSimpleName().toString())
.toList();
}

private static List<ExecutableElement> findMethods(
/**
* Finds accessible static methods on the given type taking the expected parameter types,
* regardless of their return type.
*
* @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<ExecutableElement> findMethodsStatic(
TypeElement type, List<TypeName> expectedParameterTypes, ProcessingContext context) {
return findMethods(type, expectedParameterTypes, mirror -> true, context).stream()
.filter(Predicate.not(JavaLangAnalyser::isNotStatic))
.toList();
}

/**
* 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.
*
* @param type the type element to inspect
* @param expectedParameterTypes the required parameter types
* @param expectedReturnType the required return type, or {@code null} for void-returning methods
* @param context the processing context, used to access all members
* @return the matching methods, in declaration order
*/
public static List<ExecutableElement> findMethods(
TypeElement type,
List<TypeName> expectedParameterTypes,
TypeName expectedReturnType,
ProcessingContext context) {
Predicate<TypeMirror> returnTypeMatcher =
expectedReturnType == null
? mirror -> mirror.getKind() == VOID
: mirror -> hasType(mirror, expectedReturnType);
return findMethods(type, expectedParameterTypes, returnTypeMatcher, context);
}

private static List<ExecutableElement> findMethods(
TypeElement type,
List<TypeName> expectedParameterTypes,
Predicate<TypeMirror> returnTypeMatcher,
ProcessingContext context) {
if (type == null) {
return List.of();
}
return ElementFilter.methodsIn(context.getAllMembers(type)).stream()
.filter(context::isMemberAccessibleFromBuilderPackage)
.filter(
method ->
expectedReturnType == null
? method.getReturnType().getKind() == VOID
: hasType(method.getReturnType(), expectedReturnType))
.filter(method -> returnTypeMatcher.test(method.getReturnType()))
.filter(method -> hasParameters(method, expectedParameterTypes))
.toList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.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;
Expand Down Expand Up @@ -140,8 +141,10 @@ public static TypeName mapToTypeName(TypeElement typeElement, ProcessingContext
if (typeElement == null) {
return null;
}
return new TypeName(
context.getPackageName(typeElement), typeElement.getSimpleName().toString());
String packageName = context.getPackageName(typeElement);
String qualifiedName = typeElement.getQualifiedName().toString();
String className = Strings.CS.removeStart(qualifiedName, packageName + ".");
return new TypeName(packageName, className);
}

/**
Expand Down
Loading
Loading