From f732800ac9545a4a26023b7bc946fcf638c80d4a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:42:29 +0000 Subject: [PATCH 01/14] Seed referenced builders via field functions when no copy path exists Builders lacking a seeded path (ctor(T), static T-factory, toBuilder) are now tried against field-function seeding: when every readable property of the referenced type has a matching name(v)/setName(v) function on the builder, the generated builder emits a private seed(T) method that obtains an empty builder and calls one field function per property. Builders whose properties are only partially covered still resolve but are marked degraded, so consumer helpers that would silently drop state are skipped with a debug note. --- docs/CONFIGURATION.md | 13 +- .../analysis/BuilderScopeResolver.java | 138 +++++++++++++-- .../processor/analysis/JavaLangAnalyser.java | 71 ++++++++ .../builder/FieldSeedingEnhancer.java | 83 +++++++++ .../field/NestedBuilderConsumerGenerator.java | 2 +- .../generators/util/MethodGeneratorUtil.java | 37 ++++ .../model/type/BuilderInstantiation.java | 49 ++++++ .../processor/model/type/ResolvedBuilder.java | 23 ++- ...le.builders.processor.generators.Generator | 1 + .../processor/BuilderProcessorTest.java | 4 +- .../processor/BuilderScopeProcessingTest.java | 79 +++++++++ .../processor/BuilderScopeResolverTest.java | 164 ++++++++++++------ 12 files changed, 593 insertions(+), 71 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/FieldSeedingEnhancer.java diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 1b6469dad..b4f9f650b 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -763,8 +763,10 @@ 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 builder's own contract path, a static `builder(T)`-style method, an +instance method on the value like `toBuilder()`, or — when every readable +property of the type has a matching `name(v)`/`setName(v)` field function — +seeding by field functions. The processor constructs the candidate builder name using `builderUsageSuffix` (or `builderSuffix` if not configured) and verifies the builder contract: a way @@ -773,7 +775,12 @@ factory like `create()`), a way to create one seeded with the value (a constructor accepting the referenced type or a static factory like `create(T)` or `of(T)`), and a no-arg `build()` method returning it — each accessible from the generated builder's package. Generated code calls a factory instead of -`new` when the builder offers one. Any class with the expected name and a +`new` when the builder offers one. When no seeded path exists but every +readable property has a matching field function (`name(v)` or `setName(v)`), +the generated builder seeds the instance itself in a private `seed` +method; with partial coverage the builder still resolves, but helpers that +would silently drop state (builder consumers) are skipped and a debug note is +logged. Any class with the expected name and a matching contract qualifies, allowing references to builders generated with custom template annotations, external tools, or different suffixes. If the candidate builder cannot be found, the field falls back to a plain setter. 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 f39ccd73b..81128ddca 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 @@ -23,6 +23,7 @@ */ package org.javahelpers.simple.builders.processor.analysis; +import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -34,6 +35,7 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.util.ElementFilter; +import org.apache.commons.lang3.StringUtils; 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; @@ -242,7 +244,7 @@ private Optional resolve(TypeElement referencedType) { // the builder contract. String suffix = context.getConfiguration().getBuilderUsageSuffix(); TypeName candidate = JavaLangMapper.createBuilderTypeName(referencedType, context, suffix); - return resolveByBuilderContract(candidate, referencedTypeName); + return resolveByBuilderContract(candidate, referencedType, referencedTypeName); } /** @@ -257,12 +259,13 @@ private Optional resolve(TypeElement referencedType) { * It also avoids false positives like {@code String} → {@code StringBuilder}. * * @param candidate the candidate builder type name to look up + * @param referencedType the type element being referenced * @param expectedType the referenced type the builder must accept and return * @return the resolved builder with the instantiation paths to call, or empty if no matching * builder class exists on the classpath */ private Optional resolveByBuilderContract( - TypeName candidate, TypeName expectedType) { + TypeName candidate, TypeElement referencedType, TypeName expectedType) { TypeElement builderTypeElement = context.getTypeElement(candidate.getFullQualifiedName()); if (builderTypeElement == null) { return Optional.empty(); @@ -271,20 +274,110 @@ private Optional resolveByBuilderContract( resolveFuncForEmptyBuilder(builderTypeElement); Optional funcForPrefilledBuilder = resolveFuncForPrefilledBuilder(builderTypeElement, expectedType); + if (funcForPrefilledBuilder.isEmpty() && funcForEmptyBuilder.isPresent()) { + funcForPrefilledBuilder = + resolveFieldSeeding( + referencedType, + builderTypeElement, + candidate, + expectedType, + funcForEmptyBuilder.get()); + } Optional buildMethod = findBuildMethod(builderTypeElement, expectedType); - if (funcForEmptyBuilder.isEmpty() - || funcForPrefilledBuilder.isEmpty() - || buildMethod.isEmpty()) { + if (funcForEmptyBuilder.isEmpty() || buildMethod.isEmpty()) { return Optional.empty(); } + if (funcForPrefilledBuilder.isEmpty()) { + context.debug( + "Builder %s for %s offers no way to seed a value - consumer helpers are skipped", + candidate.getFullQualifiedName(), expectedType.getFullQualifiedName()); + } return Optional.of( new ResolvedBuilder( candidate, funcForEmptyBuilder.get(), - funcForPrefilledBuilder.get(), + funcForPrefilledBuilder, buildMethod.get().getSimpleName().toString())); } + /** + * Resolves seeding by field functions: every readable property of the referenced type must have a + * matching single-parameter field function on the builder - {@code name(value)} or {@code + * setName(value)}, preferred in that order. When coverage is complete, the empty path is reused + * and the generated builder seeds the instance property by property; partial coverage resolves + * empty, so a builder whose functions would silently drop state is never picked. A type exposing + * no readable properties resolves empty as well - nothing is provably copyable. + * + * @param referencedType the type element being referenced + * @param builderElement the candidate builder type + * @param builderTypeName the candidate builder type name + * @param referencedTypeName the referenced type, seeded into the builder + * @param funcForEmptyBuilder how the seeding obtains the empty builder instance + * @return the seeding instantiation to emit, or empty when a property has no field function + */ + private Optional resolveFieldSeeding( + TypeElement referencedType, + TypeElement builderElement, + TypeName builderTypeName, + TypeName referencedTypeName, + BuilderInstantiation funcForEmptyBuilder) { + List seededFields = new ArrayList<>(); + for (JavaLangAnalyser.ReadableProperty property : + JavaLangAnalyser.findReadableProperties(referencedType, context)) { + Optional fieldFunction = findFieldFunction(builderElement, property); + if (fieldFunction.isEmpty()) { + return Optional.empty(); + } + seededFields.add( + new BuilderInstantiation.FieldSeedingCall.SeededField( + fieldFunction.get(), property.accessor())); + } + if (seededFields.isEmpty()) { + return Optional.empty(); + } + return Optional.of( + new BuilderInstantiation.FieldSeedingCall( + builderTypeName, referencedTypeName, funcForEmptyBuilder, seededFields)); + } + + /** + * Finds the field function on the builder accepting the given property: a single-parameter + * non-static method named {@code } or {@code set}, preferred in that order. + * + * @param builderElement the builder type to inspect + * @param property the readable property to match + * @return the field function's name, or empty when none matches + */ + private Optional findFieldFunction( + TypeElement builderElement, JavaLangAnalyser.ReadableProperty property) { + String setterName = "set" + StringUtils.capitalize(property.name()); + return ElementFilter.methodsIn(context.getAllMembers(builderElement)).stream() + .filter(JavaLangAnalyser::isNotStatic) + .filter(context::isMemberAccessibleFromBuilderPackage) + .filter(method -> takesProperty(method, property)) + .filter( + method -> + property.name().contentEquals(method.getSimpleName()) + || setterName.contentEquals(method.getSimpleName())) + .min( + Comparator.comparingInt( + method -> property.name().contentEquals(method.getSimpleName()) ? 0 : 1)) + .map(method -> method.getSimpleName().toString()); + } + + /** + * Checks whether the method accepts the property as its single parameter. + * + * @param method the method to check + * @param property the property whose type must be accepted + * @return true if the method takes exactly one parameter of the property's type + */ + private boolean takesProperty( + ExecutableElement method, JavaLangAnalyser.ReadableProperty property) { + return method.getParameters().size() == 1 + && context.isSameType(method.getParameters().get(0).asType(), property.typeMirror()); + } + /** * Resolves the instantiation path for an empty builder instance: a static parameterless factory * when the builder offers one, the no-arg constructor otherwise. @@ -336,7 +429,9 @@ private Optional resolveInTypeBuilder( TypeElement referencedType, TypeName referencedTypeName) { Optional nestedBuilder = nestedBuilderCandidates(referencedType).stream() - .map(candidate -> resolveByBuilderContract(candidate, referencedTypeName)) + .map( + candidate -> + resolveByBuilderContract(candidate, referencedType, referencedTypeName)) .flatMap(Optional::stream) .findFirst(); return nestedBuilder.or(() -> resolveAnchoredBuilder(referencedType, referencedTypeName)); @@ -407,14 +502,17 @@ private Optional resolveAnchoredBuilder( if (!context.isMemberAccessibleFromBuilderPackage(builderElement) || buildMethod.isEmpty()) { continue; } + BuilderInstantiation funcForEmptyBuilder = + new BuilderInstantiation.AnchorFactoryCall( + referencedTypeName, factory.getSimpleName().toString()); Optional funcForPrefilledBuilder = - resolveAnchoredPrefilledBuilder(referencedType, referencedTypeName, builderElement); + resolveAnchoredPrefilledBuilder( + referencedType, referencedTypeName, builderElement, funcForEmptyBuilder); if (funcForPrefilledBuilder.isPresent()) { return Optional.of( new ResolvedBuilder( JavaLangMapper.mapToTypeName(builderElement, context), - new BuilderInstantiation.AnchorFactoryCall( - referencedTypeName, factory.getSimpleName().toString()), + funcForEmptyBuilder, funcForPrefilledBuilder.get(), buildMethod.get().getSimpleName().toString())); } @@ -426,15 +524,19 @@ private Optional resolveAnchoredBuilder( * 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()}). + * person.toBuilder()}), then seeding by field functions. * * @param referencedType the type element being referenced * @param referencedTypeName the referenced type, passed to the contract check * @param builderElement the anchored builder type + * @param funcForEmptyBuilder how the empty builder is obtained, reused by field-function seeding * @return the instantiation to emit, or empty when no seeded path exists */ private Optional resolveAnchoredPrefilledBuilder( - TypeElement referencedType, TypeName referencedTypeName, TypeElement builderElement) { + TypeElement referencedType, + TypeName referencedTypeName, + TypeElement builderElement, + BuilderInstantiation funcForEmptyBuilder) { Optional func = resolveFuncForPrefilledBuilder(builderElement, referencedTypeName); if (func.isPresent()) { @@ -461,10 +563,18 @@ private Optional resolveAnchoredPrefilledBuilder( .min( Comparator.comparingInt( method -> "toBuilder".contentEquals(method.getSimpleName()) ? 0 : 1)) - .map( + .map( method -> new BuilderInstantiation.ValueFactoryCall( - referencedTypeName, method.getSimpleName().toString())); + referencedTypeName, method.getSimpleName().toString())) + .or( + () -> + resolveFieldSeeding( + referencedType, + builderElement, + builderTypeName, + referencedTypeName, + funcForEmptyBuilder)); } /** 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 560f56286..ca1e2eb2d 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 @@ -30,6 +30,7 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -39,6 +40,7 @@ import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.RecordComponentElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; @@ -400,6 +402,75 @@ private static boolean hasType(TypeMirror typeMirror, TypeName expectedType) { return typeMirror.toString().equals(expectedType.getFullQualifiedName()); } + /** + * A readable property of a type: the name used to match field functions ({@code name} or {@code + * setName}), how the property is read from a value, and the type it carries. + * + * @param name the property name + * @param accessor how the property is read (e.g. {@code name()}, {@code getName()} or {@code + * name}) + * @param typeMirror the property's type + */ + public record ReadableProperty(String name, String accessor, TypeMirror typeMirror) {} + + /** + * Collects the readable properties of a type in declaration order: record components, accessible + * parameterless non-static methods returning a value (getters {@code getX()}/{@code isX()} or + * record-style accessors {@code x()}) and accessible non-static fields. Method-derived property + * names strip the {@code get}/{@code is} prefix; the first source found wins when several spell + * out the same property. + * + * @param typeElement the type to inspect + * @param context the processing context, used to access all members + * @return the readable properties in declaration order + */ + public static List findReadableProperties( + TypeElement typeElement, ProcessingContext context) { + Map properties = new LinkedHashMap<>(); + for (RecordComponentElement component : typeElement.getRecordComponents()) { + String name = component.getSimpleName().toString(); + properties.put(name, new ReadableProperty(name, name + "()", component.asType())); + } + for (ExecutableElement method : ElementFilter.methodsIn(context.getAllMembers(typeElement))) { + if (isNotStatic(method) + && hasNoParameters(method) + && method.getReturnType().getKind() != VOID + && isNoMethodOfObjectClass(method, context) + && context.isMemberAccessibleFromBuilderPackage(method)) { + String name = propertyNameOf(method); + properties.putIfAbsent( + name, + new ReadableProperty(name, method.getSimpleName() + "()", method.getReturnType())); + } + } + for (VariableElement field : ElementFilter.fieldsIn(context.getAllMembers(typeElement))) { + if (!field.getModifiers().contains(STATIC) + && context.isMemberAccessibleFromBuilderPackage(field)) { + String name = field.getSimpleName().toString(); + properties.putIfAbsent(name, new ReadableProperty(name, name, field.asType())); + } + } + return List.copyOf(properties.values()); + } + + /** + * Derives the property name of an accessor method: {@code getX()}/{@code isX()} map to {@code x}, + * any other name is used unchanged. + * + * @param method the accessor method to derive the property name from + * @return the property name + */ + private static String propertyNameOf(ExecutableElement method) { + String name = method.getSimpleName().toString(); + if (name.startsWith("get") && name.length() > 3 && Character.isUpperCase(name.charAt(3))) { + return StringUtils.uncapitalize(name.substring(3)); + } + if (name.startsWith("is") && name.length() > 2 && Character.isUpperCase(name.charAt(2))) { + return StringUtils.uncapitalize(name.substring(2)); + } + return name; + } + /** * Checks whether the given type has a constructor accepting the expected type. This is part of * the builder contract used when the field already has a value that is passed to the builder. diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/FieldSeedingEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/FieldSeedingEnhancer.java new file mode 100644 index 000000000..ec28d51ed --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/FieldSeedingEnhancer.java @@ -0,0 +1,83 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.javahelpers.simple.builders.processor.generators.builder; + +import java.util.Optional; +import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; +import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; +import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.FieldDto; +import org.javahelpers.simple.builders.processor.model.type.BuilderInstantiation; +import org.javahelpers.simple.builders.processor.model.type.ResolvedBuilder; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.processing.ProcessingContext; + +/** + * Enhancer that adds the private seeding methods backing {@link + * BuilderInstantiation.FieldSeedingCall} resolutions: one {@code seed(T value)} method per + * referenced builder type, obtaining an empty builder and calling one field function per readable + * property. Several fields referencing the same type share one seeding method. + */ +public class FieldSeedingEnhancer implements BuilderEnhancer { + + private static final int PRIORITY = 15; + + @Override + public int getPriority() { + return PRIORITY; + } + + @Override + public boolean appliesTo( + BuilderDefinitionDto builderDto, TypeName dtoType, ProcessingContext context) { + return builderDto.getAllFieldsForBuilder().stream() + .anyMatch(field -> fieldSeeding(field).isPresent()); + } + + @Override + public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { + builderDto.getAllFieldsForBuilder().stream() + .map(FieldSeedingEnhancer::fieldSeeding) + .flatMap(Optional::stream) + .distinct() + .map(MethodGeneratorUtil::createFieldSeedingMethod) + .forEach(builderDto::addMethod); + } + + /** + * Returns the field-seeding instantiation of a field's resolved builder, when the seeded path is + * realized by field functions. + * + * @param field the field to inspect + * @return the field-seeding call, or empty + */ + private static Optional fieldSeeding(FieldDto field) { + return field + .getFieldType() + .getResolvedBuilder() + .flatMap(ResolvedBuilder::funcForPrefilledBuilder) + .filter(BuilderInstantiation.FieldSeedingCall.class::isInstance) + .map(BuilderInstantiation.FieldSeedingCall.class::cast); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java index ed7e45c91..059798d34 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java @@ -100,7 +100,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { Optional fieldBuilderOpt = field.getFieldType().getResolvedBuilder(); - if (fieldBuilderOpt.isEmpty()) { + if (fieldBuilderOpt.isEmpty() || fieldBuilderOpt.get().funcForPrefilledBuilder().isEmpty()) { return Collections.emptyList(); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index 8c7b6bfde..34c556c90 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -35,6 +35,7 @@ import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; +import org.javahelpers.simple.builders.processor.model.type.BuilderInstantiation; import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; import org.javahelpers.simple.builders.processor.model.type.ResolvedBuilder; import org.javahelpers.simple.builders.processor.model.type.TypeName; @@ -247,6 +248,7 @@ public static BuilderMethodDto createFieldConsumerWithBuilder( .formatted( fieldBuilder .funcForPrefilledBuilder() + .orElseThrow() .instantiationCode("$helperType:T", existingValueConstructorArgs), fieldBuilder .funcForEmptyBuilder() @@ -272,6 +274,41 @@ public static BuilderMethodDto createFieldConsumerWithBuilder( return methodDto; } + /** + * Creates the private seeding method backing a {@link BuilderInstantiation.FieldSeedingCall}: + * obtains an empty builder and calls one field function per readable property of the value. + * + * @param seedingCall the resolved field-seeding instantiation + * @return the method DTO for the seeding method + */ + public static BuilderMethodDto createFieldSeedingMethod( + BuilderInstantiation.FieldSeedingCall seedingCall) { + MethodParameterDto parameter = new MethodParameterDto(); + parameter.setParameterName("value"); + parameter.setParameterTypeName(seedingCall.sourceType()); + BuilderMethodDto methodDto = new BuilderMethodDto(); + methodDto.setModifier(AccessModifier.PRIVATE); + methodDto.setMethodName(seedingCall.methodName()); + methodDto.setReturnType(seedingCall.builderType()); + methodDto.addParameter(parameter); + StringBuilder code = new StringBuilder(); + code.append("$builderType:T builder = ") + .append(seedingCall.funcForEmptyBuilder().instantiationCode("$builderType:T", "")) + .append(";\n"); + for (BuilderInstantiation.FieldSeedingCall.SeededField seededField : + seedingCall.seededFields()) { + code.append("builder.") + .append(seededField.builderMethod()) + .append("(value.") + .append(seededField.accessor()) + .append(");\n"); + } + code.append("return builder;"); + methodDto.setCode(code.toString()); + methodDto.addArgument("builderType", seedingCall.builderType()); + return methodDto; + } + /** * Creates a Consumer type. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java index 7fed91c41..69747ec10 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java @@ -15,6 +15,8 @@ */ package org.javahelpers.simple.builders.processor.model.type; +import java.util.List; + /** * The instantiation path generated code uses to obtain an instance of a resolved builder. * @@ -112,4 +114,51 @@ public String methodReference(String builderTypeExpression) { return anchor.getFullQualifiedName() + "::" + methodName; } } + + /** + * Instantiation by seeding the builder through its field functions: a private {@code + * seed} method on the generated builder obtains an empty instance via {@code + * funcForEmptyBuilder} and calls one field function per readable property of the source value. + * {@link #instantiationCode} renders the call to that generated method. + * + * @param builderType the resolved builder type (return type of the seeding method) + * @param sourceType the referenced type the builder is seeded from + * @param funcForEmptyBuilder how the seeding method obtains the empty builder + * @param seededFields field functions to call, in the order of the source's properties + */ + record FieldSeedingCall( + TypeName builderType, + TypeName sourceType, + BuilderInstantiation funcForEmptyBuilder, + List seededFields) + implements BuilderInstantiation { + + @Override + public String instantiationCode(String builderTypeExpression, String arguments) { + return methodName() + "(" + arguments + ")"; + } + + @Override + public String methodReference(String builderTypeExpression) { + return "this::" + methodName(); + } + + /** + * The name of the seeding method generated on the target builder. + * + * @return {@code seed} followed by the builder's class name + */ + public String methodName() { + return "seed" + builderType.getClassName(); + } + + /** + * One field-function call inside the seeding method: {@code + * builder.(value.)}. + * + * @param builderMethod the field function's name on the builder + * @param accessor how the property is read from the value (e.g. {@code name()} or {@code name}) + */ + public record SeededField(String builderMethod, String accessor) {} + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java index ebac36e56..a67c08a6a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java @@ -15,6 +15,8 @@ */ package org.javahelpers.simple.builders.processor.model.type; +import java.util.Optional; + /** * A builder type resolved for a referenced field or element type, together with the instantiation * paths generated code must use to call it. @@ -26,14 +28,15 @@ * @param funcForEmptyBuilder how to obtain an empty builder instance (e.g. {@code new B()} or * {@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)}) + * new B(value)} or {@code B.of(value)}), or empty when the builder offers no seeded path; + * helpers needing one are then not generated * @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, + Optional funcForPrefilledBuilder, String buildMethodName) { /** @@ -63,6 +66,22 @@ public ResolvedBuilder( this(typeName, funcForEmptyBuilder, funcForPrefilledBuilder, "build"); } + /** + * A resolution with a custom-named terminal build method. + * + * @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 + * @param buildMethodName the name of the parameterless method returning the built value + */ + public ResolvedBuilder( + TypeName typeName, + BuilderInstantiation funcForEmptyBuilder, + BuilderInstantiation funcForPrefilledBuilder, + String buildMethodName) { + this(typeName, funcForEmptyBuilder, Optional.of(funcForPrefilledBuilder), buildMethodName); + } + /** * Renders the terminal call producing the built value: {@code * builderExpression.buildMethodName()}. diff --git a/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator b/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator index a94202381..82ff4a471 100644 --- a/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator +++ b/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator @@ -29,3 +29,4 @@ org.javahelpers.simple.builders.processor.generators.builder.CoreMethodsEnhancer org.javahelpers.simple.builders.processor.generators.builder.ConstructorEnhancer org.javahelpers.simple.builders.processor.generators.builder.WithInterfaceEnhancer org.javahelpers.simple.builders.processor.generators.builder.ConditionalEnhancer +org.javahelpers.simple.builders.processor.generators.builder.FieldSeedingEnhancer diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java index cd65dbe53..90a83d423 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java @@ -95,8 +95,8 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[DEBUG] Starting BuilderProcessor...", "[DEBUG] Loaded global configuration from compiler arguments: BuilderConfiguration[]", "[DEBUG] Initializing generator registry", - "[DEBUG] ├─ Loaded 15 method generators and 9 builder enhancers total", - "[DEBUG] └─ Initialized GeneratorRegistry with 15 method generators and 9 builder", + "[DEBUG] ├─ Loaded 15 method generators and 10 builder enhancers total", + "[DEBUG] └─ Initialized GeneratorRegistry with 15 method generators and 10 builder", // Round 1 — start "simple-builders: PROCESSING ROUND START", "[DEBUG] simple-builders: Processing round started.", diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java index 247560fb3..5e3311805 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java @@ -311,6 +311,85 @@ public static LibraryDtoBuilder of(LibraryDto v) { ProcessorAsserts.assertNotContaining(generated, "new LibraryDtoBuilder"); } + @Test + void usageScope_SeedsViaFieldFunctionsWhenNoSeededPath() { + JavaFileObject dto = dto("test", "SeedingUsageDto", "LibraryDto", "lib"); + JavaFileObject libraryDto = + ProcessorTestUtils.forSource( + """ + package lib; + public class LibraryDto { + private String name; + public String getName() { return name; } + } + """); + JavaFileObject libraryDtoBuilder = + ProcessorTestUtils.forSource( + """ + package lib; + public class LibraryDtoBuilder { + private String name; + public LibraryDtoBuilder() {} + public LibraryDtoBuilder name(String name) { this.name = name; return this; } + public LibraryDto build() { return new LibraryDto(); } + } + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions( + "-Asimplebuilder.builderGenerationPackages=test", + "-Asimplebuilder.builderUsagePackages=lib") + .compile(dto, libraryDto, libraryDtoBuilder); + + assertThat(compilation).succeeded(); + String generated = + ProcessorTestUtils.loadGeneratedSource(compilation, "SeedingUsageDtoBuilder"); + // No ctor(T) or factory(T): the generated builder seeds the referenced builder itself via + // its field functions, one per readable property + ProcessorAsserts.assertContaining( + generated, + "seedLibraryDtoBuilder(this.referenced.value())", + "private LibraryDtoBuilder seedLibraryDtoBuilder(LibraryDto value)", + "builder.name(value.getName())"); + } + + @Test + void usageScope_SkipsConsumerWhenPropertiesUncovered() { + JavaFileObject dto = dto("test", "UncoveredUsageDto", "LibraryDto", "lib"); + JavaFileObject libraryDto = + ProcessorTestUtils.forSource( + """ + package lib; + public class LibraryDto { + private String name; + public String getName() { return name; } + } + """); + JavaFileObject libraryDtoBuilder = + ProcessorTestUtils.forSource( + """ + package lib; + public class LibraryDtoBuilder { + public LibraryDtoBuilder() {} + public LibraryDto build() { return new LibraryDto(); } + } + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions( + "-Asimplebuilder.builderGenerationPackages=test", + "-Asimplebuilder.builderUsagePackages=lib") + .compile(dto, libraryDto, libraryDtoBuilder); + + assertThat(compilation).succeeded(); + // name has no field function on the builder: seeding is unsafe, so the consumer helper is + // not generated — the plain setter still is + assertNoBuilderConsumer( + compilation, "UncoveredUsageDtoBuilder", "LibraryDto", "LibraryDtoBuilder"); + } + @Test void usageScope_DoesNotReferenceBuilderWhenClassNotFoundWithUsageSuffix() { JavaFileObject dto = dto("test", "MissingBuilderDto", "LibraryDto", "lib"); 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 2e2bbf704..1a2dbd434 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 @@ -158,7 +158,7 @@ void resolverUsageScope_ResolvesContractVariants( ResolvedBuilder resolved = probeResult.get().get(); assertEquals(expectedBuilder, resolved.typeName().getFullQualifiedName()); BuilderInstantiation empty = resolved.funcForEmptyBuilder(); - BuilderInstantiation prefilled = resolved.funcForPrefilledBuilder(); + BuilderInstantiation prefilled = resolved.funcForPrefilledBuilder().get(); assertInstanceOf(expectedEmptyPath, empty); assertInstanceOf(expectedPrefilledPath, prefilled); assertFactoryMethod(expectedEmptyMethod, empty); @@ -267,7 +267,7 @@ public LibHelperBuilder(lib.LibHelper value) {} assertEquals("builder", empty.methodName()); assertEquals("lib.LibHelper", empty.anchor().getFullQualifiedName()); assertInstanceOf( - BuilderInstantiation.ConstructorCall.class, resolved.funcForPrefilledBuilder()); + BuilderInstantiation.ConstructorCall.class, resolved.funcForPrefilledBuilder().get()); } @Test @@ -346,7 +346,7 @@ private LibHelperBuilder() {} assertInstanceOf(BuilderInstantiation.AnchorFactoryCall.class, resolved.funcForEmptyBuilder()); BuilderInstantiation.AnchorFactoryCall prefilled = assertInstanceOf( - BuilderInstantiation.AnchorFactoryCall.class, resolved.funcForPrefilledBuilder()); + BuilderInstantiation.AnchorFactoryCall.class, resolved.funcForPrefilledBuilder().get()); assertEquals("builder", prefilled.methodName()); } @@ -376,7 +376,7 @@ public static class LibHelperBuilder { assertEquals("lib.LibHelper.LibHelperBuilder", resolved.typeName().getFullQualifiedName()); BuilderInstantiation.ValueFactoryCall prefilled = assertInstanceOf( - BuilderInstantiation.ValueFactoryCall.class, resolved.funcForPrefilledBuilder()); + BuilderInstantiation.ValueFactoryCall.class, resolved.funcForPrefilledBuilder().get()); assertEquals("toBuilder", prefilled.methodName()); } @@ -416,7 +416,7 @@ public Other() {} assertEquals("lib.LibHelper.LibHelperBuilder", resolved.typeName().getFullQualifiedName()); assertInstanceOf(BuilderInstantiation.ConstructorCall.class, resolved.funcForEmptyBuilder()); assertInstanceOf( - BuilderInstantiation.ConstructorCall.class, resolved.funcForPrefilledBuilder()); + BuilderInstantiation.ConstructorCall.class, resolved.funcForPrefilledBuilder().get()); } @Test @@ -446,7 +446,7 @@ public static class LibHelperBuilder { assertInstanceOf(BuilderInstantiation.AnchorFactoryCall.class, resolved.funcForEmptyBuilder()); BuilderInstantiation.ValueFactoryCall prefilled = assertInstanceOf( - BuilderInstantiation.ValueFactoryCall.class, resolved.funcForPrefilledBuilder()); + BuilderInstantiation.ValueFactoryCall.class, resolved.funcForPrefilledBuilder().get()); assertEquals("toBuilder", prefilled.methodName()); } @@ -480,7 +480,114 @@ public LibHelperBuilder(LibHelper value) {} assertEquals("lib.LibHelperBuilder", resolved.typeName().getFullQualifiedName()); assertInstanceOf(BuilderInstantiation.ConstructorCall.class, resolved.funcForEmptyBuilder()); assertInstanceOf( - BuilderInstantiation.ConstructorCall.class, resolved.funcForPrefilledBuilder()); + BuilderInstantiation.ConstructorCall.class, resolved.funcForPrefilledBuilder().get()); + } + + @Test + void resolverUsageScope_ResolvesFreeBuilderShape() { + Compilation compilation = + Compiler.javac() + .withProcessors(new ResolverProbeProcessor()) + .compile( + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelper { + public String getName() { return ""; } + public static class Builder { + public Builder() {} + public Builder mergeFrom(LibHelper value) { return this; } + public Builder name(String name) { return this; } + public Builder setName(String name) { return name(name); } + public LibHelper build() { return new LibHelper(); } + } + } + """)); + + assertThat(compilation).succeeded(); + // org.inferred.freebuilder generated shape: the nested Builder offers no ctor(T) and + // mergeFrom is an instance method, but its field functions cover every readable property, + // so the generated builder seeds the instance itself — the unprefixed name wins over setName + ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); + assertEquals("lib.LibHelper.Builder", resolved.typeName().getFullQualifiedName()); + assertInstanceOf(BuilderInstantiation.ConstructorCall.class, resolved.funcForEmptyBuilder()); + BuilderInstantiation.FieldSeedingCall prefilled = + assertInstanceOf( + BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + assertEquals("name", prefilled.seededFields().get(0).builderMethod()); + assertEquals("getName()", prefilled.seededFields().get(0).accessor()); + } + + @Test + void resolverUsageScope_ResolvesAutoValueShape() { + 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(); + } + public String getName() { return ""; } + } + """), + ProcessorTestUtils.forSource( + """ + package lib; + public class AutoValue_LibHelper { + public static class Builder { + Builder() {} + public Builder setName(String name) { return this; } + public LibHelper build() { return new LibHelper(); } + } + } + """)); + + assertThat(compilation).succeeded(); + // com.google.auto.value generated shape: create() anchors the builder on the generated + // sibling; the JavaBeans setName convention covers the readable properties, so the + // generated builder seeds the instance itself + ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); + assertEquals("lib.AutoValue_LibHelper.Builder", resolved.typeName().getFullQualifiedName()); + assertInstanceOf(BuilderInstantiation.AnchorFactoryCall.class, resolved.funcForEmptyBuilder()); + BuilderInstantiation.FieldSeedingCall prefilled = + assertInstanceOf( + BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + assertEquals("setName", prefilled.seededFields().get(0).builderMethod()); + } + + @Test + void resolverUsageScope_ResolvesWithoutSeededPathWhenPropertyUncovered() { + Compilation compilation = + Compiler.javac() + .withProcessors(new ResolverProbeProcessor()) + .compile( + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelper { + public String getName() { return ""; } + } + """), + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelperBuilder { + public LibHelperBuilder() {} + public LibHelper build() { return new LibHelper(); } + } + """)); + + assertThat(compilation).succeeded(); + // No seeded path and name has no field function: the builder still resolves, but + // funcForPrefilledBuilder stays empty so only helpers not needing a seeded path generate + ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); + assertEquals("lib.LibHelperBuilder", resolved.typeName().getFullQualifiedName()); + assertInstanceOf(BuilderInstantiation.ConstructorCall.class, resolved.funcForEmptyBuilder()); + assertEquals(Optional.empty(), resolved.funcForPrefilledBuilder()); } @Test @@ -514,7 +621,7 @@ public LibHelperBuilder(LibHelper value) {} assertEquals("lib.LibHelperBuilder", generated.typeName().getFullQualifiedName()); assertInstanceOf(BuilderInstantiation.StaticFactoryCall.class, generated.funcForEmptyBuilder()); assertInstanceOf( - BuilderInstantiation.ConstructorCall.class, generated.funcForPrefilledBuilder()); + BuilderInstantiation.ConstructorCall.class, generated.funcForPrefilledBuilder().get()); } private static final String LIB_HELPER = @@ -703,47 +810,6 @@ public static class LibHelperBuilder { } """ }), - // 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( From fc93b1918a06576bef59037fe2def97d409d945d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:44:31 +0000 Subject: [PATCH 02/14] Cover remaining readable-property shapes and tighten getter detection - propertyNameOf only strips get/is before an uppercase letter, so accessors like island()/getting() keep their own name. - Resolver tests now exercise record components, direct fields, and isX() boolean accessors on top of the getter-based shapes. --- .../processor/BuilderScopeResolverTest.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) 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 1a2dbd434..b4604726f 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 @@ -532,6 +532,7 @@ public static AutoValue_LibHelper.Builder create() { return new AutoValue_LibHelper.Builder(); } public String getName() { return ""; } + public boolean isActive() { return true; } } """), ProcessorTestUtils.forSource( @@ -541,6 +542,7 @@ public class AutoValue_LibHelper { public static class Builder { Builder() {} public Builder setName(String name) { return this; } + public Builder setActive(boolean active) { return this; } public LibHelper build() { return new LibHelper(); } } } @@ -559,6 +561,70 @@ public static class Builder { assertEquals("setName", prefilled.seededFields().get(0).builderMethod()); } + @Test + void resolverUsageScope_ResolvesRecordShape() { + Compilation compilation = + Compiler.javac() + .withProcessors(new ResolverProbeProcessor()) + .compile( + ProcessorTestUtils.forSource( + """ + package lib; + public record LibHelper(String name) {} + """), + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelperBuilder { + public LibHelperBuilder() {} + public LibHelperBuilder name(String name) { return this; } + public LibHelper build() { return new LibHelper("x"); } + } + """)); + + assertThat(compilation).succeeded(); + // Record components count as readable properties via the name() accessor + ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); + BuilderInstantiation.FieldSeedingCall prefilled = + assertInstanceOf( + BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + assertEquals("name", prefilled.seededFields().get(0).builderMethod()); + assertEquals("name()", prefilled.seededFields().get(0).accessor()); + } + + @Test + void resolverUsageScope_ResolvesFieldShape() { + Compilation compilation = + Compiler.javac() + .withProcessors(new ResolverProbeProcessor()) + .compile( + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelper { + public String name = ""; + } + """), + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelperBuilder { + public LibHelperBuilder() {} + public LibHelperBuilder name(String name) { return this; } + public LibHelper build() { return new LibHelper(); } + } + """)); + + assertThat(compilation).succeeded(); + // Accessible fields count as readable properties via direct name access + ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); + BuilderInstantiation.FieldSeedingCall prefilled = + assertInstanceOf( + BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + assertEquals("name", prefilled.seededFields().get(0).builderMethod()); + assertEquals("name", prefilled.seededFields().get(0).accessor()); + } + @Test void resolverUsageScope_ResolvesWithoutSeededPathWhenPropertyUncovered() { Compilation compilation = From 3fb31a16506eb6883e1c130d0068105b7a92c52d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:24:37 +0000 Subject: [PATCH 03/14] Detect the builder's field-function prefix instead of hardcoding name/setName The resolver now collects the builder's single-parameter methods and derives candidate naming conventions: '' (fluent name(v)) and 'set' (JavaBeans) first, then any other camel-case prefix shared by at least two methods (e.g. withName(v) as builder frameworks emit). The first prefix covering every readable property wins and is stored on FieldSeedingCall inside ResolvedBuilder; SeededField reduces to (property, accessor) and the emitted call is derived via builderMethodFor(). --- .../analysis/BuilderScopeResolver.java | 141 ++++++++++++------ .../generators/util/MethodGeneratorUtil.java | 2 +- .../model/type/BuilderInstantiation.java | 28 +++- .../processor/BuilderScopeResolverTest.java | 77 +++++++++- 4 files changed, 189 insertions(+), 59 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 81128ddca..ac258bab3 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 @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -301,12 +302,13 @@ private Optional resolveByBuilderContract( } /** - * Resolves seeding by field functions: every readable property of the referenced type must have a - * matching single-parameter field function on the builder - {@code name(value)} or {@code - * setName(value)}, preferred in that order. When coverage is complete, the empty path is reused - * and the generated builder seeds the instance property by property; partial coverage resolves - * empty, so a builder whose functions would silently drop state is never picked. A type exposing - * no readable properties resolves empty as well - nothing is provably copyable. + * Resolves seeding by field functions: the builder must offer a field function for every readable + * property of the referenced type, all sharing one naming convention - {@code name(v)}, {@code + * setName(v)}, or a detected prefix like {@code withName(v)}. When coverage is complete, the + * empty path is reused and the generated builder seeds the instance property by property; partial + * coverage resolves empty, so a builder whose functions would silently drop state is never + * picked. A type exposing no readable properties resolves empty as well - nothing is provably + * copyable. * * @param referencedType the type element being referenced * @param builderElement the candidate builder type @@ -321,61 +323,102 @@ private Optional resolveFieldSeeding( TypeName builderTypeName, TypeName referencedTypeName, BuilderInstantiation funcForEmptyBuilder) { - List seededFields = new ArrayList<>(); - for (JavaLangAnalyser.ReadableProperty property : - JavaLangAnalyser.findReadableProperties(referencedType, context)) { - Optional fieldFunction = findFieldFunction(builderElement, property); - if (fieldFunction.isEmpty()) { - return Optional.empty(); - } - seededFields.add( - new BuilderInstantiation.FieldSeedingCall.SeededField( - fieldFunction.get(), property.accessor())); - } - if (seededFields.isEmpty()) { + List properties = + JavaLangAnalyser.findReadableProperties(referencedType, context); + if (properties.isEmpty()) { return Optional.empty(); } - return Optional.of( - new BuilderInstantiation.FieldSeedingCall( - builderTypeName, referencedTypeName, funcForEmptyBuilder, seededFields)); + List fieldFunctions = + ElementFilter.methodsIn(context.getAllMembers(builderElement)).stream() + .filter(JavaLangAnalyser::isNotStatic) + .filter(method -> JavaLangAnalyser.isNoMethodOfObjectClass(method, context)) + .filter(context::isMemberAccessibleFromBuilderPackage) + .filter(method -> method.getParameters().size() == 1) + .toList(); + for (String prefix : candidatePrefixes(fieldFunctions)) { + Optional> seededFields = + matchByPrefix(properties, fieldFunctions, prefix); + if (seededFields.isPresent()) { + return Optional.of( + new BuilderInstantiation.FieldSeedingCall( + builderTypeName, + referencedTypeName, + funcForEmptyBuilder, + prefix, + seededFields.get())); + } + } + return Optional.empty(); } /** - * Finds the field function on the builder accepting the given property: a single-parameter - * non-static method named {@code } or {@code set}, preferred in that order. + * Candidate field-function prefixes, preferred first: {@code ""} (fluent {@code name(v)}), then + * {@code set} (JavaBeans), then any other camel-case prefix found on the builder's single- + * parameter methods - a convention is only trusted when several methods share it. * - * @param builderElement the builder type to inspect - * @param property the readable property to match - * @return the field function's name, or empty when none matches + * @param fieldFunctions the builder's single-parameter non-static methods + * @return the prefixes to try, in preference order */ - private Optional findFieldFunction( - TypeElement builderElement, JavaLangAnalyser.ReadableProperty property) { - String setterName = "set" + StringUtils.capitalize(property.name()); - return ElementFilter.methodsIn(context.getAllMembers(builderElement)).stream() - .filter(JavaLangAnalyser::isNotStatic) - .filter(context::isMemberAccessibleFromBuilderPackage) - .filter(method -> takesProperty(method, property)) - .filter( - method -> - property.name().contentEquals(method.getSimpleName()) - || setterName.contentEquals(method.getSimpleName())) - .min( - Comparator.comparingInt( - method -> property.name().contentEquals(method.getSimpleName()) ? 0 : 1)) - .map(method -> method.getSimpleName().toString()); + private List candidatePrefixes(List fieldFunctions) { + Map prefixCounts = new LinkedHashMap<>(); + for (ExecutableElement method : fieldFunctions) { + String name = method.getSimpleName().toString(); + int boundary = firstUpperCaseIndex(name); + if (boundary > 0) { + prefixCounts.merge(name.substring(0, boundary), 1, Integer::sum); + } + } + List prefixes = new ArrayList<>(List.of("", "set")); + prefixCounts.entrySet().stream() + .filter(entry -> entry.getValue() >= 2) + .sorted(Map.Entry.comparingByValue().reversed()) + .map(Map.Entry::getKey) + .filter(prefix -> !prefixes.contains(prefix)) + .forEach(prefixes::add); + return prefixes; + } + + private static int firstUpperCaseIndex(String name) { + for (int i = 0; i < name.length(); i++) { + if (Character.isUpperCase(name.charAt(i))) { + return i; + } + } + return -1; } /** - * Checks whether the method accepts the property as its single parameter. + * Matches every property against the builder's field functions under one naming convention: + * {@code (v)} when the prefix is empty, otherwise {@code (v)}. * - * @param method the method to check - * @param property the property whose type must be accepted - * @return true if the method takes exactly one parameter of the property's type + * @param properties the readable properties to cover + * @param fieldFunctions the builder's single-parameter non-static methods + * @param prefix the naming convention to try + * @return the seeded fields in property order when every property is covered, empty otherwise */ - private boolean takesProperty( - ExecutableElement method, JavaLangAnalyser.ReadableProperty property) { - return method.getParameters().size() == 1 - && context.isSameType(method.getParameters().get(0).asType(), property.typeMirror()); + private Optional> matchByPrefix( + List properties, + List fieldFunctions, + String prefix) { + List seededFields = new ArrayList<>(); + for (JavaLangAnalyser.ReadableProperty property : properties) { + String expected = + prefix.isEmpty() ? property.name() : prefix + StringUtils.capitalize(property.name()); + boolean covered = + fieldFunctions.stream() + .anyMatch( + method -> + expected.contentEquals(method.getSimpleName()) + && context.isSameType( + method.getParameters().get(0).asType(), property.typeMirror())); + if (!covered) { + return Optional.empty(); + } + seededFields.add( + new BuilderInstantiation.FieldSeedingCall.SeededField( + property.name(), property.accessor())); + } + return Optional.of(seededFields); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index 34c556c90..ab9360ae4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -298,7 +298,7 @@ public static BuilderMethodDto createFieldSeedingMethod( for (BuilderInstantiation.FieldSeedingCall.SeededField seededField : seedingCall.seededFields()) { code.append("builder.") - .append(seededField.builderMethod()) + .append(seedingCall.builderMethodFor(seededField)) .append("(value.") .append(seededField.accessor()) .append(");\n"); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java index 69747ec10..d0a63ca32 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java @@ -16,6 +16,7 @@ package org.javahelpers.simple.builders.processor.model.type; import java.util.List; +import org.apache.commons.lang3.StringUtils; /** * The instantiation path generated code uses to obtain an instance of a resolved builder. @@ -124,12 +125,16 @@ public String methodReference(String builderTypeExpression) { * @param builderType the resolved builder type (return type of the seeding method) * @param sourceType the referenced type the builder is seeded from * @param funcForEmptyBuilder how the seeding method obtains the empty builder - * @param seededFields field functions to call, in the order of the source's properties + * @param builderMethodPrefix the field-function prefix recognized on the builder ({@code ""} for + * {@code name(v)}, {@code set} for {@code setName(v)}, or a detected convention like {@code + * with}) + * @param seededFields the properties to seed, in the order of the source's properties */ record FieldSeedingCall( TypeName builderType, TypeName sourceType, BuilderInstantiation funcForEmptyBuilder, + String builderMethodPrefix, List seededFields) implements BuilderInstantiation { @@ -153,12 +158,25 @@ public String methodName() { } /** - * One field-function call inside the seeding method: {@code - * builder.(value.)}. + * The builder's field function for a seeded field: {@code } when the prefix is empty, + * otherwise {@code } (e.g. {@code setName} or {@code withName}). * - * @param builderMethod the field function's name on the builder + * @param field the seeded field + * @return the field function's name on the builder + */ + public String builderMethodFor(SeededField field) { + return builderMethodPrefix.isEmpty() + ? field.property() + : builderMethodPrefix + StringUtils.capitalize(field.property()); + } + + /** + * One property seeded inside the seeding method: {@code + * builder.(value.)}. + * + * @param property the property name (e.g. {@code name}) * @param accessor how the property is read from the value (e.g. {@code name()} or {@code name}) */ - public record SeededField(String builderMethod, String accessor) {} + public record SeededField(String property, String accessor) {} } } 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 b4604726f..acab79103 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 @@ -514,7 +514,8 @@ public Builder() {} BuilderInstantiation.FieldSeedingCall prefilled = assertInstanceOf( BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); - assertEquals("name", prefilled.seededFields().get(0).builderMethod()); + assertEquals("", prefilled.builderMethodPrefix()); + assertEquals("name", prefilled.seededFields().get(0).property()); assertEquals("getName()", prefilled.seededFields().get(0).accessor()); } @@ -558,7 +559,73 @@ public static class Builder { BuilderInstantiation.FieldSeedingCall prefilled = assertInstanceOf( BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); - assertEquals("setName", prefilled.seededFields().get(0).builderMethod()); + assertEquals("set", prefilled.builderMethodPrefix()); + assertEquals("name", prefilled.seededFields().get(0).property()); + } + + @Test + void resolverUsageScope_ResolvesDetectedPrefixShape() { + Compilation compilation = + Compiler.javac() + .withProcessors(new ResolverProbeProcessor()) + .compile( + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelper { + public String getName() { return ""; } + public boolean isActive() { return true; } + } + """), + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelperBuilder { + public LibHelperBuilder() {} + public LibHelperBuilder withName(String name) { return this; } + public LibHelperBuilder withActive(boolean active) { return this; } + public LibHelper build() { return new LibHelper(); } + } + """)); + + assertThat(compilation).succeeded(); + // Builder-framework style: several field functions share the camel-case prefix "with", + // which is detected as the builder's convention + ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); + BuilderInstantiation.FieldSeedingCall prefilled = + assertInstanceOf( + BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + assertEquals("with", prefilled.builderMethodPrefix()); + assertEquals(2, prefilled.seededFields().size()); + } + + @Test + void resolverUsageScope_ResolvesWithoutSeededPathWhenPrefixOccursOnce() { + Compilation compilation = + Compiler.javac() + .withProcessors(new ResolverProbeProcessor()) + .compile( + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelper { + public String getName() { return ""; } + } + """), + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelperBuilder { + public LibHelperBuilder() {} + public LibHelperBuilder putName(String name) { return this; } + public LibHelper build() { return new LibHelper(); } + } + """)); + + assertThat(compilation).succeeded(); + // A prefix seen on a single method is not trusted as a convention: no seeded path + ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); + assertEquals(Optional.empty(), resolved.funcForPrefilledBuilder()); } @Test @@ -588,7 +655,8 @@ public LibHelperBuilder() {} BuilderInstantiation.FieldSeedingCall prefilled = assertInstanceOf( BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); - assertEquals("name", prefilled.seededFields().get(0).builderMethod()); + assertEquals("", prefilled.builderMethodPrefix()); + assertEquals("name", prefilled.seededFields().get(0).property()); assertEquals("name()", prefilled.seededFields().get(0).accessor()); } @@ -621,7 +689,8 @@ public LibHelperBuilder() {} BuilderInstantiation.FieldSeedingCall prefilled = assertInstanceOf( BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); - assertEquals("name", prefilled.seededFields().get(0).builderMethod()); + assertEquals("", prefilled.builderMethodPrefix()); + assertEquals("name", prefilled.seededFields().get(0).property()); assertEquals("name", prefilled.seededFields().get(0).accessor()); } From 6272479355d9bc12df8ca3febf86143e810f1033 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:39:06 +0000 Subject: [PATCH 04/14] Rank field-function prefixes by coverage, most findings first Candidate prefixes are now sorted by how many readable properties each actually covers instead of a fixed '' -> set -> detected order; '' and 'set' remain always-tried candidates and only win on ties. The matching logic is shared by coveredCount and matchByPrefix via findFieldFunction. --- .../analysis/BuilderScopeResolver.java | 76 ++++++++++++++----- 1 file changed, 59 insertions(+), 17 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 ac258bab3..51020f7b5 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 @@ -335,7 +335,7 @@ private Optional resolveFieldSeeding( .filter(context::isMemberAccessibleFromBuilderPackage) .filter(method -> method.getParameters().size() == 1) .toList(); - for (String prefix : candidatePrefixes(fieldFunctions)) { + for (String prefix : candidatePrefixes(properties, fieldFunctions)) { Optional> seededFields = matchByPrefix(properties, fieldFunctions, prefix); if (seededFields.isPresent()) { @@ -352,14 +352,17 @@ private Optional resolveFieldSeeding( } /** - * Candidate field-function prefixes, preferred first: {@code ""} (fluent {@code name(v)}), then - * {@code set} (JavaBeans), then any other camel-case prefix found on the builder's single- - * parameter methods - a convention is only trusted when several methods share it. + * Candidate field-function prefixes ordered by how many properties each covers, most findings + * first: {@code ""} (fluent {@code name(v)}) and {@code set} (JavaBeans) are always tried, any + * other camel-case prefix found on the builder's single-parameter methods is only trusted when + * several methods share it. * + * @param properties the readable properties to cover * @param fieldFunctions the builder's single-parameter non-static methods - * @return the prefixes to try, in preference order + * @return the prefixes to try, in coverage order */ - private List candidatePrefixes(List fieldFunctions) { + private List candidatePrefixes( + List properties, List fieldFunctions) { Map prefixCounts = new LinkedHashMap<>(); for (ExecutableElement method : fieldFunctions) { String name = method.getSimpleName().toString(); @@ -371,13 +374,36 @@ private List candidatePrefixes(List fieldFunctions) { List prefixes = new ArrayList<>(List.of("", "set")); prefixCounts.entrySet().stream() .filter(entry -> entry.getValue() >= 2) - .sorted(Map.Entry.comparingByValue().reversed()) .map(Map.Entry::getKey) .filter(prefix -> !prefixes.contains(prefix)) .forEach(prefixes::add); + prefixes.sort( + Comparator.comparingInt((String prefix) -> coveredCount(properties, fieldFunctions, prefix)) + .reversed()); return prefixes; } + /** + * Counts the properties the builder covers under one naming convention. + * + * @param properties the readable properties to cover + * @param fieldFunctions the builder's single-parameter non-static methods + * @param prefix the naming convention to count + * @return how many properties have a matching field function + */ + private int coveredCount( + List properties, + List fieldFunctions, + String prefix) { + int covered = 0; + for (JavaLangAnalyser.ReadableProperty property : properties) { + if (findFieldFunction(fieldFunctions, property, prefix).isPresent()) { + covered++; + } + } + return covered; + } + private static int firstUpperCaseIndex(String name) { for (int i = 0; i < name.length(); i++) { if (Character.isUpperCase(name.charAt(i))) { @@ -402,16 +428,7 @@ private Optional> matchB String prefix) { List seededFields = new ArrayList<>(); for (JavaLangAnalyser.ReadableProperty property : properties) { - String expected = - prefix.isEmpty() ? property.name() : prefix + StringUtils.capitalize(property.name()); - boolean covered = - fieldFunctions.stream() - .anyMatch( - method -> - expected.contentEquals(method.getSimpleName()) - && context.isSameType( - method.getParameters().get(0).asType(), property.typeMirror())); - if (!covered) { + if (findFieldFunction(fieldFunctions, property, prefix).isEmpty()) { return Optional.empty(); } seededFields.add( @@ -421,6 +438,31 @@ private Optional> matchB return Optional.of(seededFields); } + /** + * Finds the builder's field function for one property under the given naming convention: a + * single-parameter method named {@code } when the prefix is empty, otherwise {@code + * }, taking the property's type. + * + * @param fieldFunctions the builder's single-parameter non-static methods + * @param property the property to match + * @param prefix the naming convention to try + * @return the matching method, or empty + */ + private Optional findFieldFunction( + List fieldFunctions, + JavaLangAnalyser.ReadableProperty property, + String prefix) { + String expected = + prefix.isEmpty() ? property.name() : prefix + StringUtils.capitalize(property.name()); + return fieldFunctions.stream() + .filter( + method -> + expected.contentEquals(method.getSimpleName()) + && context.isSameType( + method.getParameters().get(0).asType(), property.typeMirror())) + .findFirst(); + } + /** * Resolves the instantiation path for an empty builder instance: a static parameterless factory * when the builder offers one, the no-arg constructor otherwise. From 0417bcbd81cfdd2ba19d580e4733b57b4aa59aff Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:42:03 +0000 Subject: [PATCH 05/14] Scale prefix trust threshold to the property count A detected prefix now needs at least min(2, properties) occurrences, so a single-property type resolving against one withX-style method is accepted instead of rejected; for several properties full coverage already implies the count. --- .../processor/analysis/BuilderScopeResolver.java | 5 +++-- .../builders/processor/BuilderScopeResolverTest.java | 9 ++++++--- 2 files changed, 9 insertions(+), 5 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 51020f7b5..fcb105bfe 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 @@ -355,7 +355,7 @@ private Optional resolveFieldSeeding( * Candidate field-function prefixes ordered by how many properties each covers, most findings * first: {@code ""} (fluent {@code name(v)}) and {@code set} (JavaBeans) are always tried, any * other camel-case prefix found on the builder's single-parameter methods is only trusted when - * several methods share it. + * enough methods share it - one finding per property, at least two when there are several. * * @param properties the readable properties to cover * @param fieldFunctions the builder's single-parameter non-static methods @@ -372,8 +372,9 @@ private List candidatePrefixes( } } List prefixes = new ArrayList<>(List.of("", "set")); + int minOccurrences = Math.min(2, properties.size()); prefixCounts.entrySet().stream() - .filter(entry -> entry.getValue() >= 2) + .filter(entry -> entry.getValue() >= minOccurrences) .map(Map.Entry::getKey) .filter(prefix -> !prefixes.contains(prefix)) .forEach(prefixes::add); 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 acab79103..1da9525e4 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 @@ -600,7 +600,7 @@ public LibHelperBuilder() {} } @Test - void resolverUsageScope_ResolvesWithoutSeededPathWhenPrefixOccursOnce() { + void resolverUsageScope_ResolvesDetectedPrefixForSingleProperty() { Compilation compilation = Compiler.javac() .withProcessors(new ResolverProbeProcessor()) @@ -623,9 +623,12 @@ public LibHelperBuilder() {} """)); assertThat(compilation).succeeded(); - // A prefix seen on a single method is not trusted as a convention: no seeded path + // For a single-property type, one prefixed method is enough evidence for the convention ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); - assertEquals(Optional.empty(), resolved.funcForPrefilledBuilder()); + BuilderInstantiation.FieldSeedingCall prefilled = + assertInstanceOf( + BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + assertEquals("put", prefilled.builderMethodPrefix()); } @Test From 4a9a227fb6aae7d1678557c8e105ab4bb2ca0291 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:09:01 +0000 Subject: [PATCH 06/14] Gate field-function seeding behind usingFieldFunctionSeeding (default off) The seeding analysis (property enumeration + builder method scan + prefix ranking) is per-type compile cost, so it is opt-in: a new OptionState on @SimpleBuilder.Options / BuilderConfiguration / -Asimplebuilder. usingFieldFunctionSeeding, only consulted when usingExistingBuilders is enabled. Disabled, builders without a seeded path degrade as before. Resolver and processing tests enable the flag; a probe asserts the disabled gate. --- .../core/annotations/SimpleBuilder.java | 18 ++++++++ docs/CONFIGURATION.md | 30 +++++++++--- .../analysis/BuilderScopeResolver.java | 18 +++++--- .../model/core/BuilderConfiguration.java | 24 ++++++++++ .../processing/CompilerArgumentsEnum.java | 4 ++ .../processor/BuilderScopeProcessingTest.java | 6 ++- .../processor/BuilderScopeResolverTest.java | 46 ++++++++++++++++++- 7 files changed, 129 insertions(+), 17 deletions(-) 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 7ff9b14e3..52d5e6e70 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 @@ -785,6 +785,24 @@ */ OptionState usingExistingBuilders() default OptionState.UNSET; + /** + * Seed a reused builder that offers no value-accepting creation path by calling one of its + * field functions per readable property of the referenced type: the generated builder emits a + * private {@code seed(T value)} method that obtains an empty instance and calls the + * field function matching each property (fluent {@code name(v)}, JavaBeans {@code setName(v)}, + * or a detected convention like {@code withName(v)}).
+ * When disabled, such builders still resolve but helpers that would silently drop state are + * skipped. Only consulted when {@link #usingExistingBuilders()} is enabled; the analysis + * (property enumeration plus method scan per referenced type) is opt-in because it adds compile + * cost. + * + *

Default: DISABLED
+ * Compiler option: -Asimplebuilder.usingFieldFunctionSeeding + * + * @return the option state for seeding builders through field functions + */ + OptionState usingFieldFunctionSeeding() default OptionState.UNSET; + // === Naming === /** * Suffix to append to the DTO name to generate the builder class name.
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index b4f9f650b..f1e8da8b9 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -764,9 +764,8 @@ static parameterless factory on the type — `builder()` preferred — whose ret 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, an -instance method on the value like `toBuilder()`, or — when every readable -property of the type has a matching `name(v)`/`setName(v)` field function — -seeding by field functions. +instance method on the value like `toBuilder()`, or — with +`usingFieldFunctionSeeding` enabled — seeding by field functions. The processor constructs the candidate builder name using `builderUsageSuffix` (or `builderSuffix` if not configured) and verifies the builder contract: a way @@ -775,10 +774,12 @@ factory like `create()`), a way to create one seeded with the value (a constructor accepting the referenced type or a static factory like `create(T)` or `of(T)`), and a no-arg `build()` method returning it — each accessible from the generated builder's package. Generated code calls a factory instead of -`new` when the builder offers one. When no seeded path exists but every -readable property has a matching field function (`name(v)` or `setName(v)`), -the generated builder seeds the instance itself in a private `seed` -method; with partial coverage the builder still resolves, but helpers that +`new` when the builder offers one. With `usingFieldFunctionSeeding` enabled and +no seeded path found, the builder is tried against field-function seeding: the +generated builder seeds the instance itself in a private `seed` method +when every readable property has a matching field function (`name(v)`, +`setName(v)`, or a detected convention like `withName(v)`). With partial +coverage or the option disabled, the builder still resolves, but helpers that would silently drop state (builder consumers) are skipped and a debug note is logged. Any class with the expected name and a matching contract qualifies, allowing references to builders generated with @@ -812,6 +813,21 @@ type falls back to a plain setter, opting out of false-positive detections on types that coincidentally look like builders. `@SimpleMinimalBuilder` disables this option. +#### `usingFieldFunctionSeeding` + +**Default**: `DISABLED` | **Compiler Option**: `-Asimplebuilder.usingFieldFunctionSeeding=ENABLED` + +Seeds a reused builder that offers no value-accepting creation path by calling +one field function per readable property of the referenced type: the generated +builder emits a private `seed(T value)` method that obtains an empty +instance and calls the field function matching each property. The naming +convention is detected on the builder — fluent `name(v)` and JavaBeans +`setName(v)` first, then any other camel-case prefix shared by several methods +(e.g. `withName(v)`); every property must be covered by one convention, matched +param-type-exact, void- or fluent-returning. Only consulted when +`usingExistingBuilders` is enabled; the per-type analysis (property enumeration +plus method scan) is opt-in because it adds compile cost. + ### 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 fcb105bfe..f778f503b 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 @@ -275,7 +275,9 @@ private Optional resolveByBuilderContract( resolveFuncForEmptyBuilder(builderTypeElement); Optional funcForPrefilledBuilder = resolveFuncForPrefilledBuilder(builderTypeElement, expectedType); - if (funcForPrefilledBuilder.isEmpty() && funcForEmptyBuilder.isPresent()) { + if (funcForPrefilledBuilder.isEmpty() + && funcForEmptyBuilder.isPresent() + && context.getConfiguration().shouldUseFieldFunctionSeeding()) { funcForPrefilledBuilder = resolveFieldSeeding( referencedType, @@ -655,12 +657,14 @@ private Optional resolveAnchoredPrefilledBuilder( referencedTypeName, method.getSimpleName().toString())) .or( () -> - resolveFieldSeeding( - referencedType, - builderElement, - builderTypeName, - referencedTypeName, - funcForEmptyBuilder)); + context.getConfiguration().shouldUseFieldFunctionSeeding() + ? resolveFieldSeeding( + referencedType, + builderElement, + builderTypeName, + referencedTypeName, + funcForEmptyBuilder) + : Optional.empty()); } /** 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 8d98f3ddc..0e149b3de 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 @@ -73,6 +73,9 @@ * 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 usingFieldFunctionSeeding Seed a reused builder property by property through its field + * functions when it offers no value-accepting creation path; only consulted when {@code + * usingExistingBuilders} is enabled * @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} @@ -112,6 +115,7 @@ public record BuilderConfiguration( PackageScopes builderGenerationPackages, PackageScopes builderUsagePackages, OptionState usingExistingBuilders, + OptionState usingFieldFunctionSeeding, String builderSuffix, String builderUsageSuffix, String setterSuffix, @@ -156,6 +160,7 @@ public record BuilderConfiguration( .builderGenerationPackages(PackageScopes.unscoped()) .builderUsagePackages(PackageScopes.unscoped()) .usingExistingBuilders(ENABLED) + .usingFieldFunctionSeeding(DISABLED) .builderSuffix("Builder") .setterSuffix("") .formattingMode(FormattingMode.JDT.getOptionValue()) @@ -300,6 +305,10 @@ public boolean shouldUseExistingBuilders() { return usingExistingBuilders == ENABLED; } + public boolean shouldUseFieldFunctionSeeding() { + return usingFieldFunctionSeeding == ENABLED; + } + public String getBuilderSuffix() { return builderSuffix; } @@ -398,6 +407,8 @@ public BuilderConfiguration merge(BuilderConfiguration other) { .builderUsagePackages(overrideScopes(other.builderUsagePackages, this.builderUsagePackages)) .usingExistingBuilders( mergeOptionState(other.usingExistingBuilders, this.usingExistingBuilders)) + .usingFieldFunctionSeeding( + mergeOptionState(other.usingFieldFunctionSeeding, this.usingFieldFunctionSeeding)) .builderSuffix(mergeString(other.builderSuffix, this.builderSuffix)) .builderUsageSuffix(mergeString(other.builderUsageSuffix, this.builderUsageSuffix)) .setterSuffix(mergeString(other.setterSuffix, this.setterSuffix)) @@ -488,6 +499,7 @@ public String toString() { .appendIfNotEmpty("builderGenerationPackages", builderGenerationPackages.toString()) .appendIfNotEmpty("builderUsagePackages", builderUsagePackages.toString()) .appendValueIfSet("usingExistingBuilders", usingExistingBuilders) + .appendValueIfSet("usingFieldFunctionSeeding", usingFieldFunctionSeeding) .appendIfNotEmpty("builderSuffix", builderSuffix) .appendIfNotEmpty("builderUsageSuffix", builderUsageSuffix) .appendIfNotEmpty("setterSuffix", setterSuffix) @@ -584,6 +596,7 @@ public static class Builder { private PackageScopes builderGenerationPackages = null; private PackageScopes builderUsagePackages = null; private OptionState usingExistingBuilders = OptionState.UNSET; + private OptionState usingFieldFunctionSeeding = OptionState.UNSET; // === Error Handling === private OptionState strict = OptionState.UNSET; @@ -709,6 +722,16 @@ public Builder usingExistingBuilders(boolean value) { return this; } + public Builder usingFieldFunctionSeeding(OptionState value) { + this.usingFieldFunctionSeeding = value; + return this; + } + + public Builder usingFieldFunctionSeeding(boolean value) { + this.usingFieldFunctionSeeding = value ? ENABLED : DISABLED; + return this; + } + public Builder generateVarArgsHelpers(OptionState value) { this.generateVarArgsHelpers = value; return this; @@ -941,6 +964,7 @@ public BuilderConfiguration build() { builderGenerationPackages, builderUsagePackages, usingExistingBuilders, + usingFieldFunctionSeeding, 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 70bcc60db..1d12006e9 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 @@ -161,6 +161,10 @@ public enum CompilerArgumentsEnum { /** Option for reusing builders not generated by this processor. */ USING_EXISTING_BUILDERS("usingExistingBuilders", optionState(Builder::usingExistingBuilders)), + /** Option for seeding reused builders through their field functions. */ + USING_FIELD_FUNCTION_SEEDING( + "usingFieldFunctionSeeding", optionState(Builder::usingFieldFunctionSeeding)), + // === 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/BuilderScopeProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java index 5e3311805..a4a2fcf45 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java @@ -339,7 +339,8 @@ public LibraryDtoBuilder() {} ProcessorTestUtils.createCompiler() .withOptions( "-Asimplebuilder.builderGenerationPackages=test", - "-Asimplebuilder.builderUsagePackages=lib") + "-Asimplebuilder.builderUsagePackages=lib", + "-Asimplebuilder.usingFieldFunctionSeeding=ENABLED") .compile(dto, libraryDto, libraryDtoBuilder); assertThat(compilation).succeeded(); @@ -380,7 +381,8 @@ public LibraryDtoBuilder() {} ProcessorTestUtils.createCompiler() .withOptions( "-Asimplebuilder.builderGenerationPackages=test", - "-Asimplebuilder.builderUsagePackages=lib") + "-Asimplebuilder.builderUsagePackages=lib", + "-Asimplebuilder.usingFieldFunctionSeeding=ENABLED") .compile(dto, libraryDto, libraryDtoBuilder); assertThat(compilation).succeeded(); 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 1da9525e4..963520fce 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 @@ -519,6 +519,33 @@ public Builder() {} assertEquals("getName()", prefilled.seededFields().get(0).accessor()); } + @Test + void resolverUsageScope_SkipsFieldSeedingWhenDisabled() { + Compilation compilation = + Compiler.javac() + .withProcessors(new ResolverProbeProcessor()) + .compile( + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelper { + public String getName() { return ""; } + public static class Builder { + public Builder() {} + public Builder name(String name) { return this; } + public LibHelper build() { return new LibHelper(); } + } + } + """)); + + assertThat(compilation).succeeded(); + // usingFieldFunctionSeeding=DISABLED: field functions are not tried, so the builder resolves + // without a seeded path + assertEquals( + Optional.empty(), + ResolverProbeProcessor.usageSeedingDisabled.get().funcForPrefilledBuilder()); + } + @Test void resolverUsageScope_ResolvesAutoValueShape() { Compilation compilation = @@ -1154,6 +1181,7 @@ private static final class ResolverProbeProcessor extends AbstractProcessor { private static Optional usagePackagePrivate; private static Optional usageWithExistingDisabled; private static Optional usageGeneratedWithExistingDisabled; + private static Optional usageSeedingDisabled; private static Optional contractResultString; private boolean captured; @@ -1172,6 +1200,7 @@ static void reset() { usagePackagePrivate = null; usageWithExistingDisabled = null; usageGeneratedWithExistingDisabled = null; + usageSeedingDisabled = null; contractResultString = null; } @@ -1243,6 +1272,10 @@ public boolean process(Set annotations, RoundEnvironment resolver.registerGeneratedBuilder( new TypeName("lib", "LibHelper"), new TypeName("lib", "LibHelperBuilder")); usageGeneratedWithExistingDisabled = resolver.resolveUsableBuilderType(helper); + // usingFieldFunctionSeeding=DISABLED: field functions are not used as seeded path + context.initProcessingTarget(new ProcessingTarget(seedingDisabledConfiguration("lib"), "")); + resolver.resetGeneratedBuilders(); + usageSeedingDisabled = resolver.resolveUsableBuilderType(helper); // Builder-contract probe for java.lang.String: StringBuilder must not resolve because its // only parameterless method returning String is toString(), an Object signature context.initProcessingTarget(new ProcessingTarget(usageOnlyConfiguration("java.lang"), "")); @@ -1264,7 +1297,18 @@ private static BuilderConfiguration configuration(String packageName, String suf private static BuilderConfiguration usageOnlyConfiguration(String packageName) { return BuilderConfiguration.DEFAULT.merge( - BuilderConfiguration.builder().builderUsagePackages(packageName).build()); + BuilderConfiguration.builder() + .builderUsagePackages(packageName) + .usingFieldFunctionSeeding(OptionState.ENABLED) + .build()); + } + + private static BuilderConfiguration seedingDisabledConfiguration(String packageName) { + return BuilderConfiguration.DEFAULT.merge( + BuilderConfiguration.builder() + .builderUsagePackages(packageName) + .usingFieldFunctionSeeding(OptionState.DISABLED) + .build()); } private static BuilderConfiguration existingBuildersDisabledConfiguration(String packageName) { From 664de506f97c35c489c946dfad22e234066c1936 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:17:10 +0000 Subject: [PATCH 07/14] Rename seeding to prefill, majority coverage, review fixes - Emit 'prefill' (private static, ordered last) instead of 'seed' for consistency with funcForPrefilledBuilder. - Trust a field-function convention once it covers the majority of the readable properties; uncovered properties stay unset. - Reorder resolveByBuilderContract so the empty-path gate precedes the prefilled checks; factor anchored static/instance factory lookups into named methods instead of an Optional.or chain; collectSeededFields returns a list reused by coveredCount; Optional.ofNullable in ResolvedBuilder; StringUtils.substring for prefix extraction; single-colon filter predicates. --- .../core/annotations/SimpleBuilder.java | 10 +- docs/CONFIGURATION.md | 36 ++-- .../analysis/BuilderScopeResolver.java | 162 ++++++++++-------- .../generators/util/MethodGeneratorUtil.java | 8 +- .../model/type/BuilderInstantiation.java | 12 +- .../processor/model/type/ResolvedBuilder.java | 6 +- .../processor/BuilderScopeProcessingTest.java | 6 +- .../processor/BuilderScopeResolverTest.java | 38 ++++ 8 files changed, 175 insertions(+), 103 deletions(-) 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 52d5e6e70..6ff9211de 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 @@ -786,11 +786,13 @@ OptionState usingExistingBuilders() default OptionState.UNSET; /** - * Seed a reused builder that offers no value-accepting creation path by calling one of its + * Prefill a reused builder that offers no value-accepting creation path by calling one of its * field functions per readable property of the referenced type: the generated builder emits a - * private {@code seed(T value)} method that obtains an empty instance and calls the + * private {@code prefill(T value)} method that obtains an empty instance and calls the * field function matching each property (fluent {@code name(v)}, JavaBeans {@code setName(v)}, - * or a detected convention like {@code withName(v)}).
+ * or a detected convention like {@code withName(v)}). Properties without a matching function + * stay unset; the convention is trusted once it covers the majority of the readable properties. + *
* When disabled, such builders still resolve but helpers that would silently drop state are * skipped. Only consulted when {@link #usingExistingBuilders()} is enabled; the analysis * (property enumeration plus method scan per referenced type) is opt-in because it adds compile @@ -799,7 +801,7 @@ *

Default: DISABLED
* Compiler option: -Asimplebuilder.usingFieldFunctionSeeding * - * @return the option state for seeding builders through field functions + * @return the option state for prefilling builders through field functions */ OptionState usingFieldFunctionSeeding() default OptionState.UNSET; diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f1e8da8b9..6e5fb9dcf 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -775,13 +775,14 @@ constructor accepting the referenced type or a static factory like `create(T)` or `of(T)`), and a no-arg `build()` method returning it — each accessible from the generated builder's package. Generated code calls a factory instead of `new` when the builder offers one. With `usingFieldFunctionSeeding` enabled and -no seeded path found, the builder is tried against field-function seeding: the -generated builder seeds the instance itself in a private `seed` method -when every readable property has a matching field function (`name(v)`, -`setName(v)`, or a detected convention like `withName(v)`). With partial -coverage or the option disabled, the builder still resolves, but helpers that -would silently drop state (builder consumers) are skipped and a debug note is -logged. Any class with the expected name and a +no seeded path found, the builder is tried against field-function prefilling: +the generated builder emits a private static `prefill` method that +seeds each readable property having a matching field function (`name(v)`, +`setName(v)`, or a detected convention like `withName(v)`); the convention is +trusted once it covers the majority of the properties, uncovered ones stay +unset. With less coverage or the option disabled, the builder still resolves, +but helpers that would silently drop state (builder consumers) are skipped and +a debug note is logged. Any class with the expected name and a matching contract qualifies, allowing references to builders generated with custom template annotations, external tools, or different suffixes. If the candidate builder cannot be found, the field falls back to a plain setter. @@ -817,16 +818,17 @@ this option. **Default**: `DISABLED` | **Compiler Option**: `-Asimplebuilder.usingFieldFunctionSeeding=ENABLED` -Seeds a reused builder that offers no value-accepting creation path by calling -one field function per readable property of the referenced type: the generated -builder emits a private `seed(T value)` method that obtains an empty -instance and calls the field function matching each property. The naming -convention is detected on the builder — fluent `name(v)` and JavaBeans -`setName(v)` first, then any other camel-case prefix shared by several methods -(e.g. `withName(v)`); every property must be covered by one convention, matched -param-type-exact, void- or fluent-returning. Only consulted when -`usingExistingBuilders` is enabled; the per-type analysis (property enumeration -plus method scan) is opt-in because it adds compile cost. +Prefills a reused builder that offers no value-accepting creation path by +calling one field function per readable property of the referenced type: the +generated builder emits a private static `prefill(T value)` method +that obtains an empty instance and calls the field function matching each +property. The naming convention is detected on the builder — fluent `name(v)` +and JavaBeans `setName(v)` first, then any other camel-case prefix shared by +several methods (e.g. `withName(v)`); a convention is trusted once it covers +the majority of the properties, matched param-type-exact, void- or +fluent-returning. Only consulted when `usingExistingBuilders` is enabled; the +per-type analysis (property enumeration plus method scan) is opt-in because it +adds compile cost. ### Component Filtering 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 f778f503b..4e1764adf 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 @@ -273,10 +273,14 @@ private Optional resolveByBuilderContract( } Optional funcForEmptyBuilder = resolveFuncForEmptyBuilder(builderTypeElement); + Optional buildMethod = findBuildMethod(builderTypeElement, expectedType); + if (funcForEmptyBuilder.isEmpty() || buildMethod.isEmpty()) { + return Optional.empty(); + } Optional funcForPrefilledBuilder = resolveFuncForPrefilledBuilder(builderTypeElement, expectedType); + // the builder can be created empty but offers no seeded path: try its field functions if (funcForPrefilledBuilder.isEmpty() - && funcForEmptyBuilder.isPresent() && context.getConfiguration().shouldUseFieldFunctionSeeding()) { funcForPrefilledBuilder = resolveFieldSeeding( @@ -286,13 +290,9 @@ private Optional resolveByBuilderContract( expectedType, funcForEmptyBuilder.get()); } - Optional buildMethod = findBuildMethod(builderTypeElement, expectedType); - if (funcForEmptyBuilder.isEmpty() || buildMethod.isEmpty()) { - return Optional.empty(); - } if (funcForPrefilledBuilder.isEmpty()) { context.debug( - "Builder %s for %s offers no way to seed a value - consumer helpers are skipped", + "Builder %s for %s offers no way to prefill a value - consumer helpers are skipped", candidate.getFullQualifiedName(), expectedType.getFullQualifiedName()); } return Optional.of( @@ -304,20 +304,20 @@ private Optional resolveByBuilderContract( } /** - * Resolves seeding by field functions: the builder must offer a field function for every readable - * property of the referenced type, all sharing one naming convention - {@code name(v)}, {@code - * setName(v)}, or a detected prefix like {@code withName(v)}. When coverage is complete, the - * empty path is reused and the generated builder seeds the instance property by property; partial - * coverage resolves empty, so a builder whose functions would silently drop state is never - * picked. A type exposing no readable properties resolves empty as well - nothing is provably - * copyable. + * Resolves prefilling by field functions: the builder must offer a field function for the + * majority of the referenced type's readable properties, all sharing one naming convention - + * {@code name(v)}, {@code setName(v)}, or a detected prefix like {@code withName(v)}. The empty + * path is then reused and the generated builder prefills the instance property by property; + * properties the builder does not accept stay unset. Resolving less than the majority distrusts + * the convention, so a builder whose functions coincide by name is never picked. A type exposing + * no readable properties resolves empty as well - nothing is provably copyable. * * @param referencedType the type element being referenced * @param builderElement the candidate builder type * @param builderTypeName the candidate builder type name * @param referencedTypeName the referenced type, seeded into the builder * @param funcForEmptyBuilder how the seeding obtains the empty builder instance - * @return the seeding instantiation to emit, or empty when a property has no field function + * @return the seeding instantiation to emit, or empty when the convention covers too little */ private Optional resolveFieldSeeding( TypeElement referencedType, @@ -333,26 +333,31 @@ private Optional resolveFieldSeeding( List fieldFunctions = ElementFilter.methodsIn(context.getAllMembers(builderElement)).stream() .filter(JavaLangAnalyser::isNotStatic) - .filter(method -> JavaLangAnalyser.isNoMethodOfObjectClass(method, context)) + .filter(this::isNoMethodOfObjectClass) .filter(context::isMemberAccessibleFromBuilderPackage) - .filter(method -> method.getParameters().size() == 1) + .filter(this::hasSingleParameter) .toList(); for (String prefix : candidatePrefixes(properties, fieldFunctions)) { - Optional> seededFields = - matchByPrefix(properties, fieldFunctions, prefix); - if (seededFields.isPresent()) { + List seededFields = + collectSeededFields(properties, fieldFunctions, prefix); + // a convention is trusted once it covers the majority of the readable properties + if (2 * seededFields.size() > properties.size()) { return Optional.of( new BuilderInstantiation.FieldSeedingCall( - builderTypeName, - referencedTypeName, - funcForEmptyBuilder, - prefix, - seededFields.get())); + builderTypeName, referencedTypeName, funcForEmptyBuilder, prefix, seededFields)); } } return Optional.empty(); } + private boolean isNoMethodOfObjectClass(ExecutableElement method) { + return JavaLangAnalyser.isNoMethodOfObjectClass(method, context); + } + + private boolean hasSingleParameter(ExecutableElement method) { + return method.getParameters().size() == 1; + } + /** * Candidate field-function prefixes ordered by how many properties each covers, most findings * first: {@code ""} (fluent {@code name(v)}) and {@code set} (JavaBeans) are always tried, any @@ -370,7 +375,7 @@ private List candidatePrefixes( String name = method.getSimpleName().toString(); int boundary = firstUpperCaseIndex(name); if (boundary > 0) { - prefixCounts.merge(name.substring(0, boundary), 1, Integer::sum); + prefixCounts.merge(StringUtils.substring(name, 0, boundary), 1, Integer::sum); } } List prefixes = new ArrayList<>(List.of("", "set")); @@ -398,13 +403,7 @@ private int coveredCount( List properties, List fieldFunctions, String prefix) { - int covered = 0; - for (JavaLangAnalyser.ReadableProperty property : properties) { - if (findFieldFunction(fieldFunctions, property, prefix).isPresent()) { - covered++; - } - } - return covered; + return collectSeededFields(properties, fieldFunctions, prefix).size(); } private static int firstUpperCaseIndex(String name) { @@ -417,28 +416,27 @@ private static int firstUpperCaseIndex(String name) { } /** - * Matches every property against the builder's field functions under one naming convention: + * Collects the seeded fields whose properties the builder covers under one naming convention: * {@code (v)} when the prefix is empty, otherwise {@code (v)}. * * @param properties the readable properties to cover * @param fieldFunctions the builder's single-parameter non-static methods * @param prefix the naming convention to try - * @return the seeded fields in property order when every property is covered, empty otherwise + * @return the seeded fields in property order */ - private Optional> matchByPrefix( + private List collectSeededFields( List properties, List fieldFunctions, String prefix) { List seededFields = new ArrayList<>(); for (JavaLangAnalyser.ReadableProperty property : properties) { - if (findFieldFunction(fieldFunctions, property, prefix).isEmpty()) { - return Optional.empty(); + if (findFieldFunction(fieldFunctions, property, prefix).isPresent()) { + seededFields.add( + new BuilderInstantiation.FieldSeedingCall.SeededField( + property.name(), property.accessor())); } - seededFields.add( - new BuilderInstantiation.FieldSeedingCall.SeededField( - property.name(), property.accessor())); } - return Optional.of(seededFields); + return seededFields; } /** @@ -625,46 +623,72 @@ private Optional resolveAnchoredPrefilledBuilder( TypeName referencedTypeName, TypeElement builderElement, BuilderInstantiation funcForEmptyBuilder) { + TypeName builderTypeName = JavaLangMapper.mapToTypeName(builderElement, context); Optional func = resolveFuncForPrefilledBuilder(builderElement, referencedTypeName); - if (func.isPresent()) { - return func; + if (func.isEmpty()) { + func = resolveAnchoredStaticFactory(referencedType, referencedTypeName, builderTypeName); } - 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; + if (func.isEmpty()) { + func = resolveAnchoredInstanceFactory(referencedType, referencedTypeName, builderTypeName); } + if (func.isEmpty() && context.getConfiguration().shouldUseFieldFunctionSeeding()) { + func = + resolveFieldSeeding( + referencedType, + builderElement, + builderTypeName, + referencedTypeName, + funcForEmptyBuilder); + } + return func; + } + + /** + * Resolves the seeded path as a static factory anchored on the referenced type accepting the + * value (e.g. {@code Person.builder(person)}); a conventionally named {@code builder} method wins + * over other names. + * + * @param referencedType the type element being referenced + * @param referencedTypeName the referenced type, the factory's parameter + * @param builderTypeName the builder type the factory must return + * @return the instantiation calling the anchored factory, or empty + */ + private Optional resolveAnchoredStaticFactory( + TypeElement referencedType, TypeName referencedTypeName, TypeName builderTypeName) { + return 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())); + } + + /** + * Resolves the seeded path as an instance method on the value returning the builder (e.g. {@code + * person.toBuilder()}); a conventionally named {@code toBuilder} method wins over other names. + * + * @param referencedType the type element being referenced + * @param referencedTypeName the referenced type, the instance's type + * @param builderTypeName the builder type the method must return + * @return the instantiation calling the instance factory, or empty + */ + private Optional resolveAnchoredInstanceFactory( + TypeElement referencedType, TypeName referencedTypeName, TypeName builderTypeName) { return JavaLangAnalyser.findMethods(referencedType, List.of(), builderTypeName, context) .stream() .filter(JavaLangAnalyser::isNotStatic) .min( Comparator.comparingInt( method -> "toBuilder".contentEquals(method.getSimpleName()) ? 0 : 1)) - .map( + .map( method -> new BuilderInstantiation.ValueFactoryCall( - referencedTypeName, method.getSimpleName().toString())) - .or( - () -> - context.getConfiguration().shouldUseFieldFunctionSeeding() - ? resolveFieldSeeding( - referencedType, - builderElement, - builderTypeName, - referencedTypeName, - funcForEmptyBuilder) - : Optional.empty()); + referencedTypeName, method.getSimpleName().toString())); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index ab9360ae4..56da75e80 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -275,11 +275,11 @@ public static BuilderMethodDto createFieldConsumerWithBuilder( } /** - * Creates the private seeding method backing a {@link BuilderInstantiation.FieldSeedingCall}: - * obtains an empty builder and calls one field function per readable property of the value. + * Creates the private prefilling method backing a {@link BuilderInstantiation.FieldSeedingCall}: + * obtains an empty builder and calls one field function per seeded property of the value. * * @param seedingCall the resolved field-seeding instantiation - * @return the method DTO for the seeding method + * @return the method DTO for the prefilling method */ public static BuilderMethodDto createFieldSeedingMethod( BuilderInstantiation.FieldSeedingCall seedingCall) { @@ -288,6 +288,8 @@ public static BuilderMethodDto createFieldSeedingMethod( parameter.setParameterTypeName(seedingCall.sourceType()); BuilderMethodDto methodDto = new BuilderMethodDto(); methodDto.setModifier(AccessModifier.PRIVATE); + methodDto.setStatic(true); + methodDto.setOrdering(2100); // last: private helpers go below toString methodDto.setMethodName(seedingCall.methodName()); methodDto.setReturnType(seedingCall.builderType()); methodDto.addParameter(parameter); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java index d0a63ca32..c044367f6 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java @@ -117,9 +117,9 @@ public String methodReference(String builderTypeExpression) { } /** - * Instantiation by seeding the builder through its field functions: a private {@code - * seed} method on the generated builder obtains an empty instance via {@code - * funcForEmptyBuilder} and calls one field function per readable property of the source value. + * Instantiation by prefilling the builder through its field functions: a private static {@code + * prefill} method on the generated builder obtains an empty instance via {@code + * funcForEmptyBuilder} and calls one field function per seeded property of the source value. * {@link #instantiationCode} renders the call to that generated method. * * @param builderType the resolved builder type (return type of the seeding method) @@ -149,12 +149,12 @@ public String methodReference(String builderTypeExpression) { } /** - * The name of the seeding method generated on the target builder. + * The name of the prefilling method generated on the target builder. * - * @return {@code seed} followed by the builder's class name + * @return {@code prefill} followed by the builder's class name */ public String methodName() { - return "seed" + builderType.getClassName(); + return "prefill" + builderType.getClassName(); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java index a67c08a6a..6bd9c5660 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java @@ -79,7 +79,11 @@ public ResolvedBuilder( BuilderInstantiation funcForEmptyBuilder, BuilderInstantiation funcForPrefilledBuilder, String buildMethodName) { - this(typeName, funcForEmptyBuilder, Optional.of(funcForPrefilledBuilder), buildMethodName); + this( + typeName, + funcForEmptyBuilder, + Optional.ofNullable(funcForPrefilledBuilder), + buildMethodName); } /** diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java index a4a2fcf45..2bc9ebe53 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java @@ -346,12 +346,12 @@ public LibraryDtoBuilder() {} assertThat(compilation).succeeded(); String generated = ProcessorTestUtils.loadGeneratedSource(compilation, "SeedingUsageDtoBuilder"); - // No ctor(T) or factory(T): the generated builder seeds the referenced builder itself via + // No ctor(T) or factory(T): the generated builder prefills the referenced builder itself via // its field functions, one per readable property ProcessorAsserts.assertContaining( generated, - "seedLibraryDtoBuilder(this.referenced.value())", - "private LibraryDtoBuilder seedLibraryDtoBuilder(LibraryDto value)", + "prefillLibraryDtoBuilder(this.referenced.value())", + "private static LibraryDtoBuilder prefillLibraryDtoBuilder(LibraryDto value)", "builder.name(value.getName())"); } 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 963520fce..94c97ea3d 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 @@ -755,6 +755,44 @@ public LibHelperBuilder() {} assertEquals(Optional.empty(), resolved.funcForPrefilledBuilder()); } + @Test + void resolverUsageScope_SeedsCoveredFieldsWhenMajorityMatches() { + Compilation compilation = + Compiler.javac() + .withProcessors(new ResolverProbeProcessor()) + .compile( + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelper { + public String getName() { return ""; } + public boolean isActive() { return true; } + public int getAge() { return 0; } + } + """), + ProcessorTestUtils.forSource( + """ + package lib; + public class LibHelperBuilder { + public LibHelperBuilder() {} + public LibHelperBuilder name(String name) { return this; } + public LibHelperBuilder active(boolean active) { return this; } + public LibHelper build() { return new LibHelper(); } + } + """)); + + assertThat(compilation).succeeded(); + // age has no field function but two of three properties are covered: the convention is + // trusted and the covered fields are prefilled + ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); + BuilderInstantiation.FieldSeedingCall prefilled = + assertInstanceOf( + BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + assertEquals(2, prefilled.seededFields().size()); + assertEquals("name", prefilled.seededFields().get(0).property()); + assertEquals("active", prefilled.seededFields().get(1).property()); + } + @Test void resolverUsageScope_UsingExistingBuildersDisabled() { Compilation compilation = From 5bdde2b548106123850b2e49cb7d5379fe0c6bda Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:24:35 +0000 Subject: [PATCH 08/14] Rename option to usingFieldFunctionPrefill, enable by default Matches the emitted prefill naming. The option is now ENABLED in BuilderConfiguration.DEFAULT for @SimpleBuilder and explicitly DISABLED in @SimpleMinimalBuilder, keeping its feature set minimal. --- .../core/annotations/SimpleBuilder.java | 10 +++---- .../annotations/SimpleMinimalBuilder.java | 1 + docs/CONFIGURATION.md | 12 ++++---- .../analysis/BuilderScopeResolver.java | 4 +-- .../model/core/BuilderConfiguration.java | 28 +++++++++---------- .../processing/CompilerArgumentsEnum.java | 4 +-- .../processor/BuilderScopeProcessingTest.java | 4 +-- .../processor/BuilderScopeResolverTest.java | 23 +++++++-------- 8 files changed, 42 insertions(+), 44 deletions(-) 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 6ff9211de..a25aa35df 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 @@ -795,15 +795,15 @@ *
* When disabled, such builders still resolve but helpers that would silently drop state are * skipped. Only consulted when {@link #usingExistingBuilders()} is enabled; the analysis - * (property enumeration plus method scan per referenced type) is opt-in because it adds compile - * cost. + * (property enumeration plus method scan per referenced type) is a separate option because it + * adds compile cost. {@link SimpleMinimalBuilder} deactivates this option. * - *

Default: DISABLED
- * Compiler option: -Asimplebuilder.usingFieldFunctionSeeding + *

Default: ENABLED
+ * Compiler option: -Asimplebuilder.usingFieldFunctionPrefill * * @return the option state for prefilling builders through field functions */ - OptionState usingFieldFunctionSeeding() default OptionState.UNSET; + OptionState usingFieldFunctionPrefill() default OptionState.UNSET; // === Naming === /** 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 54d4777a4..937900bb3 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 @@ -95,6 +95,7 @@ copyTypeAnnotations = OptionState.DISABLED, implementsBuilderBase = OptionState.DISABLED, usingExistingBuilders = OptionState.DISABLED, + usingFieldFunctionPrefill = OptionState.DISABLED, builderSuffix = "Builder", setterSuffix = "", formattingMode = "lightweight")) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 6e5fb9dcf..d27b3aed0 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -765,7 +765,7 @@ 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, an instance method on the value like `toBuilder()`, or — with -`usingFieldFunctionSeeding` enabled — seeding by field functions. +`usingFieldFunctionPrefill` enabled — seeding by field functions. The processor constructs the candidate builder name using `builderUsageSuffix` (or `builderSuffix` if not configured) and verifies the builder contract: a way @@ -774,7 +774,7 @@ factory like `create()`), a way to create one seeded with the value (a constructor accepting the referenced type or a static factory like `create(T)` or `of(T)`), and a no-arg `build()` method returning it — each accessible from the generated builder's package. Generated code calls a factory instead of -`new` when the builder offers one. With `usingFieldFunctionSeeding` enabled and +`new` when the builder offers one. With `usingFieldFunctionPrefill` enabled and no seeded path found, the builder is tried against field-function prefilling: the generated builder emits a private static `prefill` method that seeds each readable property having a matching field function (`name(v)`, @@ -814,9 +814,9 @@ type falls back to a plain setter, opting out of false-positive detections on types that coincidentally look like builders. `@SimpleMinimalBuilder` disables this option. -#### `usingFieldFunctionSeeding` +#### `usingFieldFunctionPrefill` -**Default**: `DISABLED` | **Compiler Option**: `-Asimplebuilder.usingFieldFunctionSeeding=ENABLED` +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.usingFieldFunctionPrefill=DISABLED` Prefills a reused builder that offers no value-accepting creation path by calling one field function per readable property of the referenced type: the @@ -827,8 +827,8 @@ and JavaBeans `setName(v)` first, then any other camel-case prefix shared by several methods (e.g. `withName(v)`); a convention is trusted once it covers the majority of the properties, matched param-type-exact, void- or fluent-returning. Only consulted when `usingExistingBuilders` is enabled; the -per-type analysis (property enumeration plus method scan) is opt-in because it -adds compile cost. +per-type analysis (property enumeration plus method scan) is a separate option +because it adds compile cost. `@SimpleMinimalBuilder` disables this option. ### Component Filtering 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 4e1764adf..ab649feb6 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 @@ -281,7 +281,7 @@ private Optional resolveByBuilderContract( resolveFuncForPrefilledBuilder(builderTypeElement, expectedType); // the builder can be created empty but offers no seeded path: try its field functions if (funcForPrefilledBuilder.isEmpty() - && context.getConfiguration().shouldUseFieldFunctionSeeding()) { + && context.getConfiguration().shouldUseFieldFunctionPrefill()) { funcForPrefilledBuilder = resolveFieldSeeding( referencedType, @@ -632,7 +632,7 @@ private Optional resolveAnchoredPrefilledBuilder( if (func.isEmpty()) { func = resolveAnchoredInstanceFactory(referencedType, referencedTypeName, builderTypeName); } - if (func.isEmpty() && context.getConfiguration().shouldUseFieldFunctionSeeding()) { + if (func.isEmpty() && context.getConfiguration().shouldUseFieldFunctionPrefill()) { func = resolveFieldSeeding( referencedType, 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 0e149b3de..45edb180b 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 @@ -73,7 +73,7 @@ * 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 usingFieldFunctionSeeding Seed a reused builder property by property through its field + * @param usingFieldFunctionPrefill Prefill a reused builder property by property through its field * functions when it offers no value-accepting creation path; only consulted when {@code * usingExistingBuilders} is enabled * @param builderSuffix Suffix for builder class name @@ -115,7 +115,7 @@ public record BuilderConfiguration( PackageScopes builderGenerationPackages, PackageScopes builderUsagePackages, OptionState usingExistingBuilders, - OptionState usingFieldFunctionSeeding, + OptionState usingFieldFunctionPrefill, String builderSuffix, String builderUsageSuffix, String setterSuffix, @@ -160,7 +160,7 @@ public record BuilderConfiguration( .builderGenerationPackages(PackageScopes.unscoped()) .builderUsagePackages(PackageScopes.unscoped()) .usingExistingBuilders(ENABLED) - .usingFieldFunctionSeeding(DISABLED) + .usingFieldFunctionPrefill(ENABLED) .builderSuffix("Builder") .setterSuffix("") .formattingMode(FormattingMode.JDT.getOptionValue()) @@ -305,8 +305,8 @@ public boolean shouldUseExistingBuilders() { return usingExistingBuilders == ENABLED; } - public boolean shouldUseFieldFunctionSeeding() { - return usingFieldFunctionSeeding == ENABLED; + public boolean shouldUseFieldFunctionPrefill() { + return usingFieldFunctionPrefill == ENABLED; } public String getBuilderSuffix() { @@ -407,8 +407,8 @@ public BuilderConfiguration merge(BuilderConfiguration other) { .builderUsagePackages(overrideScopes(other.builderUsagePackages, this.builderUsagePackages)) .usingExistingBuilders( mergeOptionState(other.usingExistingBuilders, this.usingExistingBuilders)) - .usingFieldFunctionSeeding( - mergeOptionState(other.usingFieldFunctionSeeding, this.usingFieldFunctionSeeding)) + .usingFieldFunctionPrefill( + mergeOptionState(other.usingFieldFunctionPrefill, this.usingFieldFunctionPrefill)) .builderSuffix(mergeString(other.builderSuffix, this.builderSuffix)) .builderUsageSuffix(mergeString(other.builderUsageSuffix, this.builderUsageSuffix)) .setterSuffix(mergeString(other.setterSuffix, this.setterSuffix)) @@ -499,7 +499,7 @@ public String toString() { .appendIfNotEmpty("builderGenerationPackages", builderGenerationPackages.toString()) .appendIfNotEmpty("builderUsagePackages", builderUsagePackages.toString()) .appendValueIfSet("usingExistingBuilders", usingExistingBuilders) - .appendValueIfSet("usingFieldFunctionSeeding", usingFieldFunctionSeeding) + .appendValueIfSet("usingFieldFunctionPrefill", usingFieldFunctionPrefill) .appendIfNotEmpty("builderSuffix", builderSuffix) .appendIfNotEmpty("builderUsageSuffix", builderUsageSuffix) .appendIfNotEmpty("setterSuffix", setterSuffix) @@ -596,7 +596,7 @@ public static class Builder { private PackageScopes builderGenerationPackages = null; private PackageScopes builderUsagePackages = null; private OptionState usingExistingBuilders = OptionState.UNSET; - private OptionState usingFieldFunctionSeeding = OptionState.UNSET; + private OptionState usingFieldFunctionPrefill = OptionState.UNSET; // === Error Handling === private OptionState strict = OptionState.UNSET; @@ -722,13 +722,13 @@ public Builder usingExistingBuilders(boolean value) { return this; } - public Builder usingFieldFunctionSeeding(OptionState value) { - this.usingFieldFunctionSeeding = value; + public Builder usingFieldFunctionPrefill(OptionState value) { + this.usingFieldFunctionPrefill = value; return this; } - public Builder usingFieldFunctionSeeding(boolean value) { - this.usingFieldFunctionSeeding = value ? ENABLED : DISABLED; + public Builder usingFieldFunctionPrefill(boolean value) { + this.usingFieldFunctionPrefill = value ? ENABLED : DISABLED; return this; } @@ -964,7 +964,7 @@ public BuilderConfiguration build() { builderGenerationPackages, builderUsagePackages, usingExistingBuilders, - usingFieldFunctionSeeding, + usingFieldFunctionPrefill, 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 1d12006e9..a787fc7fb 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 @@ -162,8 +162,8 @@ public enum CompilerArgumentsEnum { USING_EXISTING_BUILDERS("usingExistingBuilders", optionState(Builder::usingExistingBuilders)), /** Option for seeding reused builders through their field functions. */ - USING_FIELD_FUNCTION_SEEDING( - "usingFieldFunctionSeeding", optionState(Builder::usingFieldFunctionSeeding)), + USING_FIELD_FUNCTION_PREFILL( + "usingFieldFunctionPrefill", optionState(Builder::usingFieldFunctionPrefill)), // === Naming === /** Option for builder class name suffix. */ diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java index 2bc9ebe53..78aa1fce4 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java @@ -340,7 +340,7 @@ public LibraryDtoBuilder() {} .withOptions( "-Asimplebuilder.builderGenerationPackages=test", "-Asimplebuilder.builderUsagePackages=lib", - "-Asimplebuilder.usingFieldFunctionSeeding=ENABLED") + "-Asimplebuilder.usingFieldFunctionPrefill=ENABLED") .compile(dto, libraryDto, libraryDtoBuilder); assertThat(compilation).succeeded(); @@ -382,7 +382,7 @@ public LibraryDtoBuilder() {} .withOptions( "-Asimplebuilder.builderGenerationPackages=test", "-Asimplebuilder.builderUsagePackages=lib", - "-Asimplebuilder.usingFieldFunctionSeeding=ENABLED") + "-Asimplebuilder.usingFieldFunctionPrefill=ENABLED") .compile(dto, libraryDto, libraryDtoBuilder); assertThat(compilation).succeeded(); 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 94c97ea3d..aea78c10d 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 @@ -539,11 +539,11 @@ public Builder() {} """)); assertThat(compilation).succeeded(); - // usingFieldFunctionSeeding=DISABLED: field functions are not tried, so the builder resolves + // usingFieldFunctionPrefill=DISABLED: field functions are not tried, so the builder resolves // without a seeded path assertEquals( Optional.empty(), - ResolverProbeProcessor.usageSeedingDisabled.get().funcForPrefilledBuilder()); + ResolverProbeProcessor.usagePrefillDisabled.get().funcForPrefilledBuilder()); } @Test @@ -1219,7 +1219,7 @@ private static final class ResolverProbeProcessor extends AbstractProcessor { private static Optional usagePackagePrivate; private static Optional usageWithExistingDisabled; private static Optional usageGeneratedWithExistingDisabled; - private static Optional usageSeedingDisabled; + private static Optional usagePrefillDisabled; private static Optional contractResultString; private boolean captured; @@ -1238,7 +1238,7 @@ static void reset() { usagePackagePrivate = null; usageWithExistingDisabled = null; usageGeneratedWithExistingDisabled = null; - usageSeedingDisabled = null; + usagePrefillDisabled = null; contractResultString = null; } @@ -1310,10 +1310,10 @@ public boolean process(Set annotations, RoundEnvironment resolver.registerGeneratedBuilder( new TypeName("lib", "LibHelper"), new TypeName("lib", "LibHelperBuilder")); usageGeneratedWithExistingDisabled = resolver.resolveUsableBuilderType(helper); - // usingFieldFunctionSeeding=DISABLED: field functions are not used as seeded path - context.initProcessingTarget(new ProcessingTarget(seedingDisabledConfiguration("lib"), "")); + // usingFieldFunctionPrefill=DISABLED: field functions are not used as seeded path + context.initProcessingTarget(new ProcessingTarget(prefillDisabledConfiguration("lib"), "")); resolver.resetGeneratedBuilders(); - usageSeedingDisabled = resolver.resolveUsableBuilderType(helper); + usagePrefillDisabled = resolver.resolveUsableBuilderType(helper); // Builder-contract probe for java.lang.String: StringBuilder must not resolve because its // only parameterless method returning String is toString(), an Object signature context.initProcessingTarget(new ProcessingTarget(usageOnlyConfiguration("java.lang"), "")); @@ -1335,17 +1335,14 @@ private static BuilderConfiguration configuration(String packageName, String suf private static BuilderConfiguration usageOnlyConfiguration(String packageName) { return BuilderConfiguration.DEFAULT.merge( - BuilderConfiguration.builder() - .builderUsagePackages(packageName) - .usingFieldFunctionSeeding(OptionState.ENABLED) - .build()); + BuilderConfiguration.builder().builderUsagePackages(packageName).build()); } - private static BuilderConfiguration seedingDisabledConfiguration(String packageName) { + private static BuilderConfiguration prefillDisabledConfiguration(String packageName) { return BuilderConfiguration.DEFAULT.merge( BuilderConfiguration.builder() .builderUsagePackages(packageName) - .usingFieldFunctionSeeding(OptionState.DISABLED) + .usingFieldFunctionPrefill(OptionState.DISABLED) .build()); } From 38f8a8b0c21d80679131ad63f1fc2d6b58ce378f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:27:22 +0000 Subject: [PATCH 09/14] Rename seeding vocabulary to prefill throughout FieldSeedingCall -> PrefillCall, SeededField -> PrefilledField, FieldSeedingEnhancer -> FieldPrefillEnhancer, resolveFieldSeeding -> resolveFieldFunctionPrefill, plus prose: seeded path -> prefilled path, seeding -> prefilling. Matches usingFieldFunctionPrefill and the emitted prefill methods. --- docs/CONFIGURATION.md | 12 +-- .../analysis/BuilderScopeResolver.java | 80 ++++++++++--------- ...nhancer.java => FieldPrefillEnhancer.java} | 26 +++--- .../generators/util/MethodGeneratorUtil.java | 28 +++---- .../model/type/BuilderInstantiation.java | 26 +++--- .../processor/model/type/ResolvedBuilder.java | 10 +-- .../processing/CompilerArgumentsEnum.java | 2 +- ...le.builders.processor.generators.Generator | 2 +- .../processor/BuilderScopeProcessingTest.java | 8 +- .../processor/BuilderScopeResolverTest.java | 80 +++++++++---------- 10 files changed, 138 insertions(+), 136 deletions(-) rename processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/{FieldSeedingEnhancer.java => FieldPrefillEnhancer.java} (76%) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d27b3aed0..c2406c0b3 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -762,22 +762,22 @@ 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 +style). The factory itself is the empty-instantiation path; the prefilled path is the builder's own contract path, a static `builder(T)`-style method, an instance method on the value like `toBuilder()`, or — with -`usingFieldFunctionPrefill` enabled — seeding by field functions. +`usingFieldFunctionPrefill` enabled — prefilling by field functions. 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 -factory like `create()`), a way to create one seeded with the value (a +factory like `create()`), a way to create one prefilled with the value (a constructor accepting the referenced type or a static factory like `create(T)` or `of(T)`), and a no-arg `build()` method returning it — each accessible from the generated builder's package. Generated code calls a factory instead of `new` when the builder offers one. With `usingFieldFunctionPrefill` enabled and -no seeded path found, the builder is tried against field-function prefilling: +no prefilled path found, the builder is tried against field-function prefilling: the generated builder emits a private static `prefill` method that -seeds each readable property having a matching field function (`name(v)`, +prefills each readable property having a matching field function (`name(v)`, `setName(v)`, or a detected convention like `withName(v)`); the convention is trusted once it covers the majority of the properties, uncovered ones stay unset. With less coverage or the option disabled, the builder still resolves, @@ -1090,7 +1090,7 @@ the suffix used for own builder generation. The candidate class must provide a way to create an empty builder (a no-arg constructor or a static parameterless factory like `create()`), a way to create -one seeded with the value (a constructor accepting the referenced type or a +one prefilled with the value (a constructor accepting the referenced type or a static factory like `create(T)` or `of(T)`), and a no-arg `build()` method returning it — each accessible from the generated builder's package. Generated code calls a factory instead of `new` when the builder offers one. The contract 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 ab649feb6..b8c07512e 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 @@ -107,13 +107,13 @@ public BuilderScopeResolver(ProcessingContext context) { * 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 + * Person.builder()}), with a prefilled path on the type ({@code builder(T)}/{@code * toBuilder()}) or on the builder. *

  • 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 * 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 + * instantiation path prefilled with the value (constructor accepting the referenced type or * 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 @@ -251,13 +251,13 @@ private Optional 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 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}. + * factory like {@code create()}), a way to create an instance prefilled with a value (a + * constructor 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 referencedType the type element being referenced @@ -279,11 +279,11 @@ private Optional resolveByBuilderContract( } Optional funcForPrefilledBuilder = resolveFuncForPrefilledBuilder(builderTypeElement, expectedType); - // the builder can be created empty but offers no seeded path: try its field functions + // the builder can be created empty but offers no prefilled path: try its field functions if (funcForPrefilledBuilder.isEmpty() && context.getConfiguration().shouldUseFieldFunctionPrefill()) { funcForPrefilledBuilder = - resolveFieldSeeding( + resolveFieldFunctionPrefill( referencedType, builderTypeElement, candidate, @@ -315,11 +315,11 @@ private Optional resolveByBuilderContract( * @param referencedType the type element being referenced * @param builderElement the candidate builder type * @param builderTypeName the candidate builder type name - * @param referencedTypeName the referenced type, seeded into the builder - * @param funcForEmptyBuilder how the seeding obtains the empty builder instance - * @return the seeding instantiation to emit, or empty when the convention covers too little + * @param referencedTypeName the referenced type, prefilled into the builder + * @param funcForEmptyBuilder how the prefilling obtains the empty builder instance + * @return the prefilling instantiation to emit, or empty when the convention covers too little */ - private Optional resolveFieldSeeding( + private Optional resolveFieldFunctionPrefill( TypeElement referencedType, TypeElement builderElement, TypeName builderTypeName, @@ -338,13 +338,13 @@ private Optional resolveFieldSeeding( .filter(this::hasSingleParameter) .toList(); for (String prefix : candidatePrefixes(properties, fieldFunctions)) { - List seededFields = - collectSeededFields(properties, fieldFunctions, prefix); + List prefilledFields = + collectPrefilledFields(properties, fieldFunctions, prefix); // a convention is trusted once it covers the majority of the readable properties - if (2 * seededFields.size() > properties.size()) { + if (2 * prefilledFields.size() > properties.size()) { return Optional.of( - new BuilderInstantiation.FieldSeedingCall( - builderTypeName, referencedTypeName, funcForEmptyBuilder, prefix, seededFields)); + new BuilderInstantiation.PrefillCall( + builderTypeName, referencedTypeName, funcForEmptyBuilder, prefix, prefilledFields)); } } return Optional.empty(); @@ -403,7 +403,7 @@ private int coveredCount( List properties, List fieldFunctions, String prefix) { - return collectSeededFields(properties, fieldFunctions, prefix).size(); + return collectPrefilledFields(properties, fieldFunctions, prefix).size(); } private static int firstUpperCaseIndex(String name) { @@ -416,27 +416,27 @@ private static int firstUpperCaseIndex(String name) { } /** - * Collects the seeded fields whose properties the builder covers under one naming convention: + * Collects the prefilled fields whose properties the builder covers under one naming convention: * {@code (v)} when the prefix is empty, otherwise {@code (v)}. * * @param properties the readable properties to cover * @param fieldFunctions the builder's single-parameter non-static methods * @param prefix the naming convention to try - * @return the seeded fields in property order + * @return the prefilled fields in property order */ - private List collectSeededFields( + private List collectPrefilledFields( List properties, List fieldFunctions, String prefix) { - List seededFields = new ArrayList<>(); + List prefilledFields = new ArrayList<>(); for (JavaLangAnalyser.ReadableProperty property : properties) { if (findFieldFunction(fieldFunctions, property, prefix).isPresent()) { - seededFields.add( - new BuilderInstantiation.FieldSeedingCall.SeededField( + prefilledFields.add( + new BuilderInstantiation.PrefillCall.PrefilledField( property.name(), property.accessor())); } } - return seededFields; + return prefilledFields; } /** @@ -481,12 +481,12 @@ private Optional resolveFuncForEmptyBuilder( } /** - * Resolves the instantiation path for a builder instance seeded with a value of the referenced + * Resolves the instantiation path for a builder instance prefilled with a value of the referenced * type: a static factory accepting the type when the builder offers one, the constructor * accepting the type otherwise. * * @param builderTypeElement the candidate builder type to inspect - * @param expectedType the referenced type to seed the builder with + * @param expectedType the referenced type to prefill the builder with * @return the instantiation to emit, or empty when the builder offers neither */ private Optional resolveFuncForPrefilledBuilder( @@ -607,16 +607,17 @@ private Optional resolveAnchoredBuilder( } /** - * 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 + * Resolves the prefilled-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()}), then seeding by field functions. + * person.toBuilder()}), then prefilling by field functions. * * @param referencedType the type element being referenced * @param referencedTypeName the referenced type, passed to the contract check * @param builderElement the anchored builder type - * @param funcForEmptyBuilder how the empty builder is obtained, reused by field-function seeding - * @return the instantiation to emit, or empty when no seeded path exists + * @param funcForEmptyBuilder how the empty builder is obtained, reused by field-function + * prefilling + * @return the instantiation to emit, or empty when no prefilled path exists */ private Optional resolveAnchoredPrefilledBuilder( TypeElement referencedType, @@ -634,7 +635,7 @@ private Optional resolveAnchoredPrefilledBuilder( } if (func.isEmpty() && context.getConfiguration().shouldUseFieldFunctionPrefill()) { func = - resolveFieldSeeding( + resolveFieldFunctionPrefill( referencedType, builderElement, builderTypeName, @@ -645,7 +646,7 @@ private Optional resolveAnchoredPrefilledBuilder( } /** - * Resolves the seeded path as a static factory anchored on the referenced type accepting the + * Resolves the prefilled path as a static factory anchored on the referenced type accepting the * value (e.g. {@code Person.builder(person)}); a conventionally named {@code builder} method wins * over other names. * @@ -669,8 +670,9 @@ private Optional resolveAnchoredStaticFactory( } /** - * Resolves the seeded path as an instance method on the value returning the builder (e.g. {@code - * person.toBuilder()}); a conventionally named {@code toBuilder} method wins over other names. + * Resolves the prefilled path as an instance method on the value returning the builder (e.g. + * {@code person.toBuilder()}); a conventionally named {@code toBuilder} method wins over other + * names. * * @param referencedType the type element being referenced * @param referencedTypeName the referenced type, the instance's type diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/FieldSeedingEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/FieldPrefillEnhancer.java similarity index 76% rename from processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/FieldSeedingEnhancer.java rename to processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/FieldPrefillEnhancer.java index ec28d51ed..d9c3e61eb 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/FieldSeedingEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/FieldPrefillEnhancer.java @@ -34,12 +34,12 @@ import org.javahelpers.simple.builders.processor.processing.ProcessingContext; /** - * Enhancer that adds the private seeding methods backing {@link - * BuilderInstantiation.FieldSeedingCall} resolutions: one {@code seed(T value)} method per + * Enhancer that adds the private prefilling methods backing {@link + * BuilderInstantiation.PrefillCall} resolutions: one {@code prefill(T value)} method per * referenced builder type, obtaining an empty builder and calling one field function per readable - * property. Several fields referencing the same type share one seeding method. + * property. Several fields referencing the same type share one prefilling method. */ -public class FieldSeedingEnhancer implements BuilderEnhancer { +public class FieldPrefillEnhancer implements BuilderEnhancer { private static final int PRIORITY = 15; @@ -52,32 +52,32 @@ public int getPriority() { public boolean appliesTo( BuilderDefinitionDto builderDto, TypeName dtoType, ProcessingContext context) { return builderDto.getAllFieldsForBuilder().stream() - .anyMatch(field -> fieldSeeding(field).isPresent()); + .anyMatch(field -> fieldPrefill(field).isPresent()); } @Override public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { builderDto.getAllFieldsForBuilder().stream() - .map(FieldSeedingEnhancer::fieldSeeding) + .map(FieldPrefillEnhancer::fieldPrefill) .flatMap(Optional::stream) .distinct() - .map(MethodGeneratorUtil::createFieldSeedingMethod) + .map(MethodGeneratorUtil::createFieldPrefillMethod) .forEach(builderDto::addMethod); } /** - * Returns the field-seeding instantiation of a field's resolved builder, when the seeded path is - * realized by field functions. + * Returns the field-prefilling instantiation of a field's resolved builder, when the prefilled + * path is realized by field functions. * * @param field the field to inspect - * @return the field-seeding call, or empty + * @return the field-prefilling call, or empty */ - private static Optional fieldSeeding(FieldDto field) { + private static Optional fieldPrefill(FieldDto field) { return field .getFieldType() .getResolvedBuilder() .flatMap(ResolvedBuilder::funcForPrefilledBuilder) - .filter(BuilderInstantiation.FieldSeedingCall.class::isInstance) - .map(BuilderInstantiation.FieldSeedingCall.class::cast); + .filter(BuilderInstantiation.PrefillCall.class::isInstance) + .map(BuilderInstantiation.PrefillCall.class::cast); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index 56da75e80..58fb44287 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -275,39 +275,39 @@ public static BuilderMethodDto createFieldConsumerWithBuilder( } /** - * Creates the private prefilling method backing a {@link BuilderInstantiation.FieldSeedingCall}: - * obtains an empty builder and calls one field function per seeded property of the value. + * Creates the private prefilling method backing a {@link BuilderInstantiation.PrefillCall}: + * obtains an empty builder and calls one field function per prefilled property of the value. * - * @param seedingCall the resolved field-seeding instantiation + * @param prefillCall the resolved field-prefilling instantiation * @return the method DTO for the prefilling method */ - public static BuilderMethodDto createFieldSeedingMethod( - BuilderInstantiation.FieldSeedingCall seedingCall) { + public static BuilderMethodDto createFieldPrefillMethod( + BuilderInstantiation.PrefillCall prefillCall) { MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName("value"); - parameter.setParameterTypeName(seedingCall.sourceType()); + parameter.setParameterTypeName(prefillCall.sourceType()); BuilderMethodDto methodDto = new BuilderMethodDto(); methodDto.setModifier(AccessModifier.PRIVATE); methodDto.setStatic(true); methodDto.setOrdering(2100); // last: private helpers go below toString - methodDto.setMethodName(seedingCall.methodName()); - methodDto.setReturnType(seedingCall.builderType()); + methodDto.setMethodName(prefillCall.methodName()); + methodDto.setReturnType(prefillCall.builderType()); methodDto.addParameter(parameter); StringBuilder code = new StringBuilder(); code.append("$builderType:T builder = ") - .append(seedingCall.funcForEmptyBuilder().instantiationCode("$builderType:T", "")) + .append(prefillCall.funcForEmptyBuilder().instantiationCode("$builderType:T", "")) .append(";\n"); - for (BuilderInstantiation.FieldSeedingCall.SeededField seededField : - seedingCall.seededFields()) { + for (BuilderInstantiation.PrefillCall.PrefilledField prefilledField : + prefillCall.prefilledFields()) { code.append("builder.") - .append(seedingCall.builderMethodFor(seededField)) + .append(prefillCall.builderMethodFor(prefilledField)) .append("(value.") - .append(seededField.accessor()) + .append(prefilledField.accessor()) .append(");\n"); } code.append("return builder;"); methodDto.setCode(code.toString()); - methodDto.addArgument("builderType", seedingCall.builderType()); + methodDto.addArgument("builderType", prefillCall.builderType()); return methodDto; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java index c044367f6..06e3ebac8 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java @@ -119,23 +119,23 @@ public String methodReference(String builderTypeExpression) { /** * Instantiation by prefilling the builder through its field functions: a private static {@code * prefill} method on the generated builder obtains an empty instance via {@code - * funcForEmptyBuilder} and calls one field function per seeded property of the source value. + * funcForEmptyBuilder} and calls one field function per prefilled property of the source value. * {@link #instantiationCode} renders the call to that generated method. * - * @param builderType the resolved builder type (return type of the seeding method) - * @param sourceType the referenced type the builder is seeded from - * @param funcForEmptyBuilder how the seeding method obtains the empty builder + * @param builderType the resolved builder type (return type of the prefilling method) + * @param sourceType the referenced type the builder is prefilled from + * @param funcForEmptyBuilder how the prefilling method obtains the empty builder * @param builderMethodPrefix the field-function prefix recognized on the builder ({@code ""} for * {@code name(v)}, {@code set} for {@code setName(v)}, or a detected convention like {@code * with}) - * @param seededFields the properties to seed, in the order of the source's properties + * @param prefilledFields the properties to prefill, in the order of the source's properties */ - record FieldSeedingCall( + record PrefillCall( TypeName builderType, TypeName sourceType, BuilderInstantiation funcForEmptyBuilder, String builderMethodPrefix, - List seededFields) + List prefilledFields) implements BuilderInstantiation { @Override @@ -158,25 +158,25 @@ public String methodName() { } /** - * The builder's field function for a seeded field: {@code } when the prefix is empty, - * otherwise {@code } (e.g. {@code setName} or {@code withName}). + * The builder's field function for a prefilled field: {@code } when the prefix is + * empty, otherwise {@code } (e.g. {@code setName} or {@code withName}). * - * @param field the seeded field + * @param field the prefilled field * @return the field function's name on the builder */ - public String builderMethodFor(SeededField field) { + public String builderMethodFor(PrefilledField field) { return builderMethodPrefix.isEmpty() ? field.property() : builderMethodPrefix + StringUtils.capitalize(field.property()); } /** - * One property seeded inside the seeding method: {@code + * One property prefilled inside the prefilling method: {@code * builder.(value.)}. * * @param property the property name (e.g. {@code name}) * @param accessor how the property is read from the value (e.g. {@code name()} or {@code name}) */ - public record SeededField(String property, String accessor) {} + public record PrefilledField(String property, String accessor) {} } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java index 6bd9c5660..a279746cf 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/ResolvedBuilder.java @@ -27,9 +27,9 @@ * @param typeName the builder type to reference * @param funcForEmptyBuilder how to obtain an empty builder instance (e.g. {@code new B()} or * {@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)}), or empty when the builder offers no seeded path; - * helpers needing one are then not generated + * @param funcForPrefilledBuilder how to obtain a builder instance prefilled with a value (e.g. + * {@code new B(value)} or {@code B.of(value)}), or empty when the builder offers no prefilled + * path; helpers needing one are then not generated * @param buildMethodName the name of the parameterless method returning the built value (e.g. * {@code build}) */ @@ -57,7 +57,7 @@ public ResolvedBuilder(TypeName typeName) { * * @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 + * @param funcForPrefilledBuilder how to obtain a builder instance prefilled with a value */ public ResolvedBuilder( TypeName typeName, @@ -71,7 +71,7 @@ public ResolvedBuilder( * * @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 + * @param funcForPrefilledBuilder how to obtain a builder instance prefilled with a value * @param buildMethodName the name of the parameterless method returning the built value */ public ResolvedBuilder( 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 a787fc7fb..6fa81777e 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 @@ -161,7 +161,7 @@ public enum CompilerArgumentsEnum { /** Option for reusing builders not generated by this processor. */ USING_EXISTING_BUILDERS("usingExistingBuilders", optionState(Builder::usingExistingBuilders)), - /** Option for seeding reused builders through their field functions. */ + /** Option for prefilling reused builders through their field functions. */ USING_FIELD_FUNCTION_PREFILL( "usingFieldFunctionPrefill", optionState(Builder::usingFieldFunctionPrefill)), diff --git a/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator b/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator index 82ff4a471..3bad8341a 100644 --- a/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator +++ b/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator @@ -29,4 +29,4 @@ org.javahelpers.simple.builders.processor.generators.builder.CoreMethodsEnhancer org.javahelpers.simple.builders.processor.generators.builder.ConstructorEnhancer org.javahelpers.simple.builders.processor.generators.builder.WithInterfaceEnhancer org.javahelpers.simple.builders.processor.generators.builder.ConditionalEnhancer -org.javahelpers.simple.builders.processor.generators.builder.FieldSeedingEnhancer +org.javahelpers.simple.builders.processor.generators.builder.FieldPrefillEnhancer diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java index 78aa1fce4..d8a5a2a26 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java @@ -312,8 +312,8 @@ public static LibraryDtoBuilder of(LibraryDto v) { } @Test - void usageScope_SeedsViaFieldFunctionsWhenNoSeededPath() { - JavaFileObject dto = dto("test", "SeedingUsageDto", "LibraryDto", "lib"); + void usageScope_PrefillsViaFieldFunctionsWhenNoPrefilledPath() { + JavaFileObject dto = dto("test", "PrefillUsageDto", "LibraryDto", "lib"); JavaFileObject libraryDto = ProcessorTestUtils.forSource( """ @@ -345,7 +345,7 @@ public LibraryDtoBuilder() {} assertThat(compilation).succeeded(); String generated = - ProcessorTestUtils.loadGeneratedSource(compilation, "SeedingUsageDtoBuilder"); + ProcessorTestUtils.loadGeneratedSource(compilation, "PrefillUsageDtoBuilder"); // No ctor(T) or factory(T): the generated builder prefills the referenced builder itself via // its field functions, one per readable property ProcessorAsserts.assertContaining( @@ -386,7 +386,7 @@ public LibraryDtoBuilder() {} .compile(dto, libraryDto, libraryDtoBuilder); assertThat(compilation).succeeded(); - // name has no field function on the builder: seeding is unsafe, so the consumer helper is + // name has no field function on the builder: prefilling is unsafe, so the consumer helper is // not generated — the plain setter still is assertNoBuilderConsumer( compilation, "UncoveredUsageDtoBuilder", "LibraryDto", "LibraryDtoBuilder"); 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 aea78c10d..107acac3f 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 @@ -258,7 +258,7 @@ public LibHelperBuilder(lib.LibHelper value) {} assertThat(compilation).succeeded(); // The type's own builder() declaration anchors the builder, even in another package: - // the empty path calls LibHelper.builder(), the seeded path the builder's ctor + // the empty path calls LibHelper.builder(), the prefilled path the builder's ctor ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); assertEquals("builders.LibHelperBuilder", resolved.typeName().getFullQualifiedName()); BuilderInstantiation.AnchorFactoryCall empty = @@ -315,7 +315,7 @@ public LibHelperBuilder(lib.LibHelper value) {} } @Test - void resolverUsageScope_ResolvesStaticSeededPath() { + void resolverUsageScope_ResolvesStaticPrefilledPath() { Compilation compilation = Compiler.javac() .withProcessors(new ResolverProbeProcessor()) @@ -339,7 +339,7 @@ private LibHelperBuilder() {} """)); assertThat(compilation).succeeded(); - // No seeded path on the builder itself: the type's static builder(T) becomes the seeded + // No prefilled path on the builder itself: the type's static builder(T) becomes the prefilled // path — two candidates match, the conventional name wins ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); assertEquals("lib.LibHelper.LibHelperBuilder", resolved.typeName().getFullQualifiedName()); @@ -351,7 +351,7 @@ private LibHelperBuilder() {} } @Test - void resolverUsageScope_InstanceSeededPathPrefersToBuilder() { + void resolverUsageScope_InstancePrefilledPathPrefersToBuilder() { Compilation compilation = Compiler.javac() .withProcessors(new ResolverProbeProcessor()) @@ -440,7 +440,7 @@ public static class LibHelperBuilder { assertThat(compilation).succeeded(); // Delombok output of @Builder(toBuilder = true): builder() anchors the empty path, - // toBuilder() the seeded one; the nested builder's package-private ctor is never called + // toBuilder() the prefilled one; the nested builder's package-private ctor is never called ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); assertEquals("lib.LibHelper.LibHelperBuilder", resolved.typeName().getFullQualifiedName()); assertInstanceOf(BuilderInstantiation.AnchorFactoryCall.class, resolved.funcForEmptyBuilder()); @@ -507,20 +507,20 @@ public Builder() {} assertThat(compilation).succeeded(); // org.inferred.freebuilder generated shape: the nested Builder offers no ctor(T) and // mergeFrom is an instance method, but its field functions cover every readable property, - // so the generated builder seeds the instance itself — the unprefixed name wins over setName + // so the generated builder prefills the instance itself — the unprefixed name wins over setName ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); assertEquals("lib.LibHelper.Builder", resolved.typeName().getFullQualifiedName()); assertInstanceOf(BuilderInstantiation.ConstructorCall.class, resolved.funcForEmptyBuilder()); - BuilderInstantiation.FieldSeedingCall prefilled = + BuilderInstantiation.PrefillCall prefilled = assertInstanceOf( - BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + BuilderInstantiation.PrefillCall.class, resolved.funcForPrefilledBuilder().get()); assertEquals("", prefilled.builderMethodPrefix()); - assertEquals("name", prefilled.seededFields().get(0).property()); - assertEquals("getName()", prefilled.seededFields().get(0).accessor()); + assertEquals("name", prefilled.prefilledFields().get(0).property()); + assertEquals("getName()", prefilled.prefilledFields().get(0).accessor()); } @Test - void resolverUsageScope_SkipsFieldSeedingWhenDisabled() { + void resolverUsageScope_SkipsFieldPrefillWhenDisabled() { Compilation compilation = Compiler.javac() .withProcessors(new ResolverProbeProcessor()) @@ -540,7 +540,7 @@ public Builder() {} assertThat(compilation).succeeded(); // usingFieldFunctionPrefill=DISABLED: field functions are not tried, so the builder resolves - // without a seeded path + // without a prefilled path assertEquals( Optional.empty(), ResolverProbeProcessor.usagePrefillDisabled.get().funcForPrefilledBuilder()); @@ -579,15 +579,15 @@ public static class Builder { assertThat(compilation).succeeded(); // com.google.auto.value generated shape: create() anchors the builder on the generated // sibling; the JavaBeans setName convention covers the readable properties, so the - // generated builder seeds the instance itself + // generated builder prefills the instance itself ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); assertEquals("lib.AutoValue_LibHelper.Builder", resolved.typeName().getFullQualifiedName()); assertInstanceOf(BuilderInstantiation.AnchorFactoryCall.class, resolved.funcForEmptyBuilder()); - BuilderInstantiation.FieldSeedingCall prefilled = + BuilderInstantiation.PrefillCall prefilled = assertInstanceOf( - BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + BuilderInstantiation.PrefillCall.class, resolved.funcForPrefilledBuilder().get()); assertEquals("set", prefilled.builderMethodPrefix()); - assertEquals("name", prefilled.seededFields().get(0).property()); + assertEquals("name", prefilled.prefilledFields().get(0).property()); } @Test @@ -619,11 +619,11 @@ public LibHelperBuilder() {} // Builder-framework style: several field functions share the camel-case prefix "with", // which is detected as the builder's convention ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); - BuilderInstantiation.FieldSeedingCall prefilled = + BuilderInstantiation.PrefillCall prefilled = assertInstanceOf( - BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + BuilderInstantiation.PrefillCall.class, resolved.funcForPrefilledBuilder().get()); assertEquals("with", prefilled.builderMethodPrefix()); - assertEquals(2, prefilled.seededFields().size()); + assertEquals(2, prefilled.prefilledFields().size()); } @Test @@ -652,9 +652,9 @@ public LibHelperBuilder() {} assertThat(compilation).succeeded(); // For a single-property type, one prefixed method is enough evidence for the convention ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); - BuilderInstantiation.FieldSeedingCall prefilled = + BuilderInstantiation.PrefillCall prefilled = assertInstanceOf( - BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + BuilderInstantiation.PrefillCall.class, resolved.funcForPrefilledBuilder().get()); assertEquals("put", prefilled.builderMethodPrefix()); } @@ -682,12 +682,12 @@ public LibHelperBuilder() {} assertThat(compilation).succeeded(); // Record components count as readable properties via the name() accessor ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); - BuilderInstantiation.FieldSeedingCall prefilled = + BuilderInstantiation.PrefillCall prefilled = assertInstanceOf( - BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + BuilderInstantiation.PrefillCall.class, resolved.funcForPrefilledBuilder().get()); assertEquals("", prefilled.builderMethodPrefix()); - assertEquals("name", prefilled.seededFields().get(0).property()); - assertEquals("name()", prefilled.seededFields().get(0).accessor()); + assertEquals("name", prefilled.prefilledFields().get(0).property()); + assertEquals("name()", prefilled.prefilledFields().get(0).accessor()); } @Test @@ -716,16 +716,16 @@ public LibHelperBuilder() {} assertThat(compilation).succeeded(); // Accessible fields count as readable properties via direct name access ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); - BuilderInstantiation.FieldSeedingCall prefilled = + BuilderInstantiation.PrefillCall prefilled = assertInstanceOf( - BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); + BuilderInstantiation.PrefillCall.class, resolved.funcForPrefilledBuilder().get()); assertEquals("", prefilled.builderMethodPrefix()); - assertEquals("name", prefilled.seededFields().get(0).property()); - assertEquals("name", prefilled.seededFields().get(0).accessor()); + assertEquals("name", prefilled.prefilledFields().get(0).property()); + assertEquals("name", prefilled.prefilledFields().get(0).accessor()); } @Test - void resolverUsageScope_ResolvesWithoutSeededPathWhenPropertyUncovered() { + void resolverUsageScope_ResolvesWithoutPrefilledPathWhenPropertyUncovered() { Compilation compilation = Compiler.javac() .withProcessors(new ResolverProbeProcessor()) @@ -747,8 +747,8 @@ public LibHelperBuilder() {} """)); assertThat(compilation).succeeded(); - // No seeded path and name has no field function: the builder still resolves, but - // funcForPrefilledBuilder stays empty so only helpers not needing a seeded path generate + // No prefilled path and name has no field function: the builder still resolves, but + // funcForPrefilledBuilder stays empty so only helpers not needing a prefilled path generate ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); assertEquals("lib.LibHelperBuilder", resolved.typeName().getFullQualifiedName()); assertInstanceOf(BuilderInstantiation.ConstructorCall.class, resolved.funcForEmptyBuilder()); @@ -756,7 +756,7 @@ public LibHelperBuilder() {} } @Test - void resolverUsageScope_SeedsCoveredFieldsWhenMajorityMatches() { + void resolverUsageScope_PrefillsCoveredFieldsWhenMajorityMatches() { Compilation compilation = Compiler.javac() .withProcessors(new ResolverProbeProcessor()) @@ -785,12 +785,12 @@ public LibHelperBuilder() {} // age has no field function but two of three properties are covered: the convention is // trusted and the covered fields are prefilled ResolvedBuilder resolved = ResolverProbeProcessor.usageWithoutAnnotation.get(); - BuilderInstantiation.FieldSeedingCall prefilled = + BuilderInstantiation.PrefillCall prefilled = assertInstanceOf( - BuilderInstantiation.FieldSeedingCall.class, resolved.funcForPrefilledBuilder().get()); - assertEquals(2, prefilled.seededFields().size()); - assertEquals("name", prefilled.seededFields().get(0).property()); - assertEquals("active", prefilled.seededFields().get(1).property()); + BuilderInstantiation.PrefillCall.class, resolved.funcForPrefilledBuilder().get()); + assertEquals(2, prefilled.prefilledFields().size()); + assertEquals("name", prefilled.prefilledFields().get(0).property()); + assertEquals("active", prefilled.prefilledFields().get(1).property()); } @Test @@ -997,7 +997,7 @@ public NoContract() {} """ }), // Delombok output of @Builder without toBuilder: builder() anchors the empty path, - // but no seeded path exists, so the anchored candidate is rejected + // but no prefilled path exists, so the anchored candidate is rejected Arguments.argumentSet( "LombokBuilderShapeWithoutToBuilder", (Object) @@ -1310,7 +1310,7 @@ public boolean process(Set annotations, RoundEnvironment resolver.registerGeneratedBuilder( new TypeName("lib", "LibHelper"), new TypeName("lib", "LibHelperBuilder")); usageGeneratedWithExistingDisabled = resolver.resolveUsableBuilderType(helper); - // usingFieldFunctionPrefill=DISABLED: field functions are not used as seeded path + // usingFieldFunctionPrefill=DISABLED: field functions are not used as prefilled path context.initProcessingTarget(new ProcessingTarget(prefillDisabledConfiguration("lib"), "")); resolver.resetGeneratedBuilders(); usagePrefillDisabled = resolver.resolveUsableBuilderType(helper); From d5b005d3ef27eb58232bbf36ad6faa524ec38b17 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:31:37 +0000 Subject: [PATCH 10/14] StringUtils.indexOfAny for prefix boundary, Predicate.not for filter Review fixes: replace the hand-rolled firstUpperCaseIndex loop with StringUtils.indexOfAny, and the negated lambda with Predicate.not(prefixes::contains). --- .../processor/analysis/BuilderScopeResolver.java | 15 ++++----------- 1 file changed, 4 insertions(+), 11 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 b8c07512e..aa55943a8 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 @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Predicate; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; @@ -68,6 +69,7 @@ private record ResolutionInputs(BuilderConfiguration configuration, String build // Factory method names preferred when a builder offers several candidates private static final List PREFERRED_FACTORY_NAMES = List.of("create", "of"); + private static final char[] UPPERCASE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); private final ProcessingContext context; private ResolutionInputs cachedResolutionInputs; @@ -373,7 +375,7 @@ private List candidatePrefixes( Map prefixCounts = new LinkedHashMap<>(); for (ExecutableElement method : fieldFunctions) { String name = method.getSimpleName().toString(); - int boundary = firstUpperCaseIndex(name); + int boundary = StringUtils.indexOfAny(name, UPPERCASE_CHARS); if (boundary > 0) { prefixCounts.merge(StringUtils.substring(name, 0, boundary), 1, Integer::sum); } @@ -383,7 +385,7 @@ private List candidatePrefixes( prefixCounts.entrySet().stream() .filter(entry -> entry.getValue() >= minOccurrences) .map(Map.Entry::getKey) - .filter(prefix -> !prefixes.contains(prefix)) + .filter(Predicate.not(prefixes::contains)) .forEach(prefixes::add); prefixes.sort( Comparator.comparingInt((String prefix) -> coveredCount(properties, fieldFunctions, prefix)) @@ -406,15 +408,6 @@ private int coveredCount( return collectPrefilledFields(properties, fieldFunctions, prefix).size(); } - private static int firstUpperCaseIndex(String name) { - for (int i = 0; i < name.length(); i++) { - if (Character.isUpperCase(name.charAt(i))) { - return i; - } - } - return -1; - } - /** * Collects the prefilled fields whose properties the builder covers under one naming convention: * {@code (v)} when the prefix is empty, otherwise {@code (v)}. From 1b49d97aee67b0e7d09bb1276b1c3bf2238a7b66 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 20:51:53 +0000 Subject: [PATCH 11/14] Fix Sonar findings: extract container unwrap, stream collectPrefilledFields, guard record components - S3776: extract unwrapContainerInstances from findAnnotationInstances (cognitive complexity 19 -> under limit) - S9391: stream pipeline in collectPrefilledFields - S2259: Optional.ofNullable around getRecordComponents() --- .../analysis/BuilderScopeResolver.java | 16 +++---- .../processor/analysis/JavaLangAnalyser.java | 43 +++++++++++++------ 2 files changed, 36 insertions(+), 23 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 aa55943a8..d4c4baad4 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 @@ -421,15 +421,13 @@ private List collectPrefilledFi List properties, List fieldFunctions, String prefix) { - List prefilledFields = new ArrayList<>(); - for (JavaLangAnalyser.ReadableProperty property : properties) { - if (findFieldFunction(fieldFunctions, property, prefix).isPresent()) { - prefilledFields.add( - new BuilderInstantiation.PrefillCall.PrefilledField( - property.name(), property.accessor())); - } - } - return prefilledFields; + return properties.stream() + .filter(property -> findFieldFunction(fieldFunctions, property, prefix).isPresent()) + .map( + property -> + new BuilderInstantiation.PrefillCall.PrefilledField( + property.name(), property.accessor())) + .toList(); } /** 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 ca1e2eb2d..4e4c684ff 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 @@ -251,31 +251,45 @@ public static List findAnnotationInstances( Class annotationClass, Class containerClass, ProcessingContext context) { - List instances = new ArrayList<>(); if (element == null) { - return instances; + return List.of(); } + List instances = new ArrayList<>(); for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { String annotationName = annotationMirror.getAnnotationType().toString(); if (annotationName.equals(annotationClass.getCanonicalName())) { instances.add(annotationMirror); } else if (annotationName.equals(containerClass.getCanonicalName())) { - Optional containerValue = - findAnnotationAttribute(annotationMirror, "value", context); - if (containerValue.isPresent() - && containerValue.get().getValue() instanceof List entries) { - for (Object entry : entries) { - if (entry instanceof AnnotationValue entryValue - && entryValue.getValue() instanceof AnnotationMirror entryMirror) { - instances.add(entryMirror); - } - } - } + instances.addAll(unwrapContainerInstances(annotationMirror, context)); } } return instances; } + /** + * Unwraps the annotation instances repeated inside a container annotation's {@code value} + * attribute. + * + * @param containerMirror the container annotation to unwrap + * @param context the processing context providing element utilities + * @return the annotation mirrors wrapped by the container + */ + private static List unwrapContainerInstances( + AnnotationMirror containerMirror, ProcessingContext context) { + Optional containerValue = + findAnnotationAttribute(containerMirror, "value", context); + if (containerValue.isEmpty() || !(containerValue.get().getValue() instanceof List entries)) { + return List.of(); + } + return entries.stream() + .filter(AnnotationValue.class::isInstance) + .map(AnnotationValue.class::cast) + .map(AnnotationValue::getValue) + .filter(AnnotationMirror.class::isInstance) + .map(AnnotationMirror.class::cast) + .toList(); + } + /** * Reads the value of one attribute of an annotation mirror, including default values. * @@ -427,7 +441,8 @@ public record ReadableProperty(String name, String accessor, TypeMirror typeMirr public static List findReadableProperties( TypeElement typeElement, ProcessingContext context) { Map properties = new LinkedHashMap<>(); - for (RecordComponentElement component : typeElement.getRecordComponents()) { + for (RecordComponentElement component : + Optional.ofNullable(typeElement.getRecordComponents()).orElse(List.of())) { String name = component.getSimpleName().toString(); properties.put(name, new ReadableProperty(name, name + "()", component.asType())); } From dbe7406cfb352d6229948a33ed8910ab3d0520c4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:12:58 +0000 Subject: [PATCH 12/14] Null-check type parameter in findReadableProperties (S2259) The flagged deref was the receiver, not the record-components list: sibling finders already guard a null type the same way. --- .../simple/builders/processor/analysis/JavaLangAnalyser.java | 3 +++ 1 file changed, 3 insertions(+) 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 4e4c684ff..5c5b02614 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 @@ -440,6 +440,9 @@ public record ReadableProperty(String name, String accessor, TypeMirror typeMirr */ public static List findReadableProperties( TypeElement typeElement, ProcessingContext context) { + if (typeElement == null) { + return List.of(); + } Map properties = new LinkedHashMap<>(); for (RecordComponentElement component : Optional.ofNullable(typeElement.getRecordComponents()).orElse(List.of())) { From cd40a005fdaf4bcd6cc46c4fb0ad07e4dd8b80d6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:23:38 +0000 Subject: [PATCH 13/14] Gate debug note, move field-function finder to analyser, pick best convention - 'consumer helpers are skipped' debug only when usingFieldFunctionPrefill is enabled - fieldFunctions collection becomes JavaLangAnalyser.findFieldFunctions - all candidate conventions are scored and the best-covered one wins; Optional.empty only when the best misses the majority - empty/set keep tie priority through the stable sort --- .../analysis/BuilderScopeResolver.java | 83 ++++++++----------- .../processor/analysis/JavaLangAnalyser.java | 19 +++++ 2 files changed, 55 insertions(+), 47 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 d4c4baad4..2fc1fc875 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 @@ -292,7 +292,8 @@ private Optional resolveByBuilderContract( expectedType, funcForEmptyBuilder.get()); } - if (funcForPrefilledBuilder.isEmpty()) { + if (funcForPrefilledBuilder.isEmpty() + && context.getConfiguration().shouldUseFieldFunctionPrefill()) { context.debug( "Builder %s for %s offers no way to prefill a value - consumer helpers are skipped", candidate.getFullQualifiedName(), expectedType.getFullQualifiedName()); @@ -333,42 +334,48 @@ private Optional resolveFieldFunctionPrefill( return Optional.empty(); } List fieldFunctions = - ElementFilter.methodsIn(context.getAllMembers(builderElement)).stream() - .filter(JavaLangAnalyser::isNotStatic) - .filter(this::isNoMethodOfObjectClass) - .filter(context::isMemberAccessibleFromBuilderPackage) - .filter(this::hasSingleParameter) - .toList(); - for (String prefix : candidatePrefixes(properties, fieldFunctions)) { - List prefilledFields = - collectPrefilledFields(properties, fieldFunctions, prefix); - // a convention is trusted once it covers the majority of the readable properties - if (2 * prefilledFields.size() > properties.size()) { - return Optional.of( - new BuilderInstantiation.PrefillCall( - builderTypeName, referencedTypeName, funcForEmptyBuilder, prefix, prefilledFields)); - } + JavaLangAnalyser.findFieldFunctions(builderElement, context); + ConventionCoverage best = + candidatePrefixes(properties, fieldFunctions).stream() + .map( + prefix -> + new ConventionCoverage( + prefix, collectPrefilledFields(properties, fieldFunctions, prefix))) + .sorted( + Comparator.comparingInt( + (ConventionCoverage convention) -> convention.prefilledFields().size()) + .reversed()) + .findFirst() + .orElseThrow(); + // a convention is trusted once it covers the majority of the readable properties + if (2 * best.prefilledFields().size() <= properties.size()) { + return Optional.empty(); } - return Optional.empty(); - } - - private boolean isNoMethodOfObjectClass(ExecutableElement method) { - return JavaLangAnalyser.isNoMethodOfObjectClass(method, context); + return Optional.of( + new BuilderInstantiation.PrefillCall( + builderTypeName, + referencedTypeName, + funcForEmptyBuilder, + best.prefix(), + best.prefilledFields())); } - private boolean hasSingleParameter(ExecutableElement method) { - return method.getParameters().size() == 1; - } + /** + * A naming convention paired with the prefilled fields the builder covers under it: the + * convention the most properties follow wins. + */ + private record ConventionCoverage( + String prefix, List prefilledFields) {} /** - * Candidate field-function prefixes ordered by how many properties each covers, most findings - * first: {@code ""} (fluent {@code name(v)}) and {@code set} (JavaBeans) are always tried, any - * other camel-case prefix found on the builder's single-parameter methods is only trusted when - * enough methods share it - one finding per property, at least two when there are several. + * Candidate field-function prefixes: {@code ""} (fluent {@code name(v)}) and {@code set} + * (JavaBeans) are always tried, any other camel-case prefix found on the builder's + * single-parameter methods is only trusted when enough methods share it - one finding per + * property, at least two when there are several. * * @param properties the readable properties to cover * @param fieldFunctions the builder's single-parameter non-static methods - * @return the prefixes to try, in coverage order + * @return the prefixes to evaluate */ private List candidatePrefixes( List properties, List fieldFunctions) { @@ -387,27 +394,9 @@ private List candidatePrefixes( .map(Map.Entry::getKey) .filter(Predicate.not(prefixes::contains)) .forEach(prefixes::add); - prefixes.sort( - Comparator.comparingInt((String prefix) -> coveredCount(properties, fieldFunctions, prefix)) - .reversed()); return prefixes; } - /** - * Counts the properties the builder covers under one naming convention. - * - * @param properties the readable properties to cover - * @param fieldFunctions the builder's single-parameter non-static methods - * @param prefix the naming convention to count - * @return how many properties have a matching field function - */ - private int coveredCount( - List properties, - List fieldFunctions, - String prefix) { - return collectPrefilledFields(properties, fieldFunctions, prefix).size(); - } - /** * Collects the prefilled fields whose properties the builder covers under one naming convention: * {@code (v)} when the prefix is empty, otherwise {@code (v)}. 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 5c5b02614..8ccc5a9a1 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 @@ -471,6 +471,25 @@ && isNoMethodOfObjectClass(method, context) return List.copyOf(properties.values()); } + /** + * Finds the builder's field functions: accessible non-static single-parameter methods that are + * not members of {@link Object}. These are the candidates a naming convention may use to prefill + * the builder property by property. + * + * @param type the builder type to inspect + * @param context the processing context, used to access all members + * @return the field functions in declaration order + */ + public static List findFieldFunctions( + TypeElement type, ProcessingContext context) { + return ElementFilter.methodsIn(context.getAllMembers(type)).stream() + .filter(JavaLangAnalyser::isNotStatic) + .filter(method -> isNoMethodOfObjectClass(method, context)) + .filter(context::isMemberAccessibleFromBuilderPackage) + .filter(method -> method.getParameters().size() == 1) + .toList(); + } + /** * Derives the property name of an accessor method: {@code getX()}/{@code isX()} map to {@code x}, * any other name is used unchanged. From 4d98ec65c8890c36d6e9488ccca84657deff4a15 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:27:20 +0000 Subject: [PATCH 14/14] PrefillCall.Field naming, static factory, real builders in fixtures - rename PrefilledField to Field: PrefillCall.Field carries the context - static prefilledField factory makes the coverage map a :: reference - test fixture builders now retain their values so build() produces a populated instance - the mock builders act as real builders --- .../analysis/BuilderScopeResolver.java | 21 +++- .../generators/util/MethodGeneratorUtil.java | 3 +- .../model/type/BuilderInstantiation.java | 6 +- .../processor/BuilderScopeProcessingTest.java | 5 +- .../processor/BuilderScopeResolverTest.java | 116 +++++++++++++----- 5 files changed, 108 insertions(+), 43 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 2fc1fc875..313739622 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 @@ -365,7 +365,7 @@ prefix, collectPrefilledFields(properties, fieldFunctions, prefix))) * convention the most properties follow wins. */ private record ConventionCoverage( - String prefix, List prefilledFields) {} + String prefix, List prefilledFields) {} /** * Candidate field-function prefixes: {@code ""} (fluent {@code name(v)}) and {@code set} @@ -406,19 +406,28 @@ private List candidatePrefixes( * @param prefix the naming convention to try * @return the prefilled fields in property order */ - private List collectPrefilledFields( + private List collectPrefilledFields( List properties, List fieldFunctions, String prefix) { return properties.stream() .filter(property -> findFieldFunction(fieldFunctions, property, prefix).isPresent()) - .map( - property -> - new BuilderInstantiation.PrefillCall.PrefilledField( - property.name(), property.accessor())) + .map(BuilderScopeResolver::prefilledField) .toList(); } + /** + * Creates the prefilled field for a property the builder covers: the property name plus how it is + * read from the source value. + * + * @param property the readable property to prefill + * @return the prefilled field to emit + */ + private static BuilderInstantiation.PrefillCall.Field prefilledField( + JavaLangAnalyser.ReadableProperty property) { + return new BuilderInstantiation.PrefillCall.Field(property.name(), property.accessor()); + } + /** * Finds the builder's field function for one property under the given naming convention: a * single-parameter method named {@code } when the prefix is empty, otherwise {@code diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index 58fb44287..fa190c465 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -297,8 +297,7 @@ public static BuilderMethodDto createFieldPrefillMethod( code.append("$builderType:T builder = ") .append(prefillCall.funcForEmptyBuilder().instantiationCode("$builderType:T", "")) .append(";\n"); - for (BuilderInstantiation.PrefillCall.PrefilledField prefilledField : - prefillCall.prefilledFields()) { + for (BuilderInstantiation.PrefillCall.Field prefilledField : prefillCall.prefilledFields()) { code.append("builder.") .append(prefillCall.builderMethodFor(prefilledField)) .append("(value.") diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java index 06e3ebac8..583546be9 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderInstantiation.java @@ -135,7 +135,7 @@ record PrefillCall( TypeName sourceType, BuilderInstantiation funcForEmptyBuilder, String builderMethodPrefix, - List prefilledFields) + List prefilledFields) implements BuilderInstantiation { @Override @@ -164,7 +164,7 @@ public String methodName() { * @param field the prefilled field * @return the field function's name on the builder */ - public String builderMethodFor(PrefilledField field) { + public String builderMethodFor(Field field) { return builderMethodPrefix.isEmpty() ? field.property() : builderMethodPrefix + StringUtils.capitalize(field.property()); @@ -177,6 +177,6 @@ public String builderMethodFor(PrefilledField field) { * @param property the property name (e.g. {@code name}) * @param accessor how the property is read from the value (e.g. {@code name()} or {@code name}) */ - public record PrefilledField(String property, String accessor) {} + public record Field(String property, String accessor) {} } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java index d8a5a2a26..7e6f57703 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeProcessingTest.java @@ -319,7 +319,8 @@ void usageScope_PrefillsViaFieldFunctionsWhenNoPrefilledPath() { """ package lib; public class LibraryDto { - private String name; + private final String name; + public LibraryDto(String name) { this.name = name; } public String getName() { return name; } } """); @@ -331,7 +332,7 @@ public class LibraryDtoBuilder { private String name; public LibraryDtoBuilder() {} public LibraryDtoBuilder name(String name) { this.name = name; return this; } - public LibraryDto build() { return new LibraryDto(); } + public LibraryDto build() { return new LibraryDto(name); } } """); 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 107acac3f..dfd6f73e8 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 @@ -493,13 +493,22 @@ void resolverUsageScope_ResolvesFreeBuilderShape() { """ package lib; public class LibHelper { - public String getName() { return ""; } + private String name; + public String getName() { return name; } public static class Builder { + private String name; public Builder() {} - public Builder mergeFrom(LibHelper value) { return this; } - public Builder name(String name) { return this; } + public Builder mergeFrom(LibHelper value) { + name = value.getName(); + return this; + } + public Builder name(String name) { this.name = name; return this; } public Builder setName(String name) { return name(name); } - public LibHelper build() { return new LibHelper(); } + public LibHelper build() { + LibHelper helper = new LibHelper(); + helper.name = name; + return helper; + } } } """)); @@ -529,11 +538,17 @@ void resolverUsageScope_SkipsFieldPrefillWhenDisabled() { """ package lib; public class LibHelper { - public String getName() { return ""; } + private String name; + public String getName() { return name; } public static class Builder { + private String name; public Builder() {} - public Builder name(String name) { return this; } - public LibHelper build() { return new LibHelper(); } + public Builder name(String name) { this.name = name; return this; } + public LibHelper build() { + LibHelper helper = new LibHelper(); + helper.name = name; + return helper; + } } } """)); @@ -556,11 +571,17 @@ void resolverUsageScope_ResolvesAutoValueShape() { """ package lib; public class LibHelper { + private final String name; + private final boolean active; + public LibHelper(String name, boolean active) { + this.name = name; + this.active = active; + } public static AutoValue_LibHelper.Builder create() { return new AutoValue_LibHelper.Builder(); } - public String getName() { return ""; } - public boolean isActive() { return true; } + public String getName() { return name; } + public boolean isActive() { return active; } } """), ProcessorTestUtils.forSource( @@ -568,10 +589,12 @@ public static AutoValue_LibHelper.Builder create() { package lib; public class AutoValue_LibHelper { public static class Builder { + private String name; + private boolean active; Builder() {} - public Builder setName(String name) { return this; } - public Builder setActive(boolean active) { return this; } - public LibHelper build() { return new LibHelper(); } + public Builder setName(String name) { this.name = name; return this; } + public Builder setActive(boolean active) { this.active = active; return this; } + public LibHelper build() { return new LibHelper(name, active); } } } """)); @@ -600,18 +623,29 @@ void resolverUsageScope_ResolvesDetectedPrefixShape() { """ package lib; public class LibHelper { - public String getName() { return ""; } - public boolean isActive() { return true; } + private final String name; + private final boolean active; + public LibHelper(String name, boolean active) { + this.name = name; + this.active = active; + } + public String getName() { return name; } + public boolean isActive() { return active; } } """), ProcessorTestUtils.forSource( """ package lib; public class LibHelperBuilder { + private String name; + private boolean active; public LibHelperBuilder() {} - public LibHelperBuilder withName(String name) { return this; } - public LibHelperBuilder withActive(boolean active) { return this; } - public LibHelper build() { return new LibHelper(); } + public LibHelperBuilder withName(String name) { this.name = name; return this; } + public LibHelperBuilder withActive(boolean active) { + this.active = active; + return this; + } + public LibHelper build() { return new LibHelper(name, active); } } """)); @@ -636,16 +670,19 @@ void resolverUsageScope_ResolvesDetectedPrefixForSingleProperty() { """ package lib; public class LibHelper { - public String getName() { return ""; } + private final String name; + public LibHelper(String name) { this.name = name; } + public String getName() { return name; } } """), ProcessorTestUtils.forSource( """ package lib; public class LibHelperBuilder { + private String name; public LibHelperBuilder() {} - public LibHelperBuilder putName(String name) { return this; } - public LibHelper build() { return new LibHelper(); } + public LibHelperBuilder putName(String name) { this.name = name; return this; } + public LibHelper build() { return new LibHelper(name); } } """)); @@ -673,9 +710,10 @@ public record LibHelper(String name) {} """ package lib; public class LibHelperBuilder { + private String name; public LibHelperBuilder() {} - public LibHelperBuilder name(String name) { return this; } - public LibHelper build() { return new LibHelper("x"); } + public LibHelperBuilder name(String name) { this.name = name; return this; } + public LibHelper build() { return new LibHelper(name); } } """)); @@ -707,9 +745,14 @@ public class LibHelper { """ package lib; public class LibHelperBuilder { + private String name; public LibHelperBuilder() {} - public LibHelperBuilder name(String name) { return this; } - public LibHelper build() { return new LibHelper(); } + public LibHelperBuilder name(String name) { this.name = name; return this; } + public LibHelper build() { + LibHelper helper = new LibHelper(); + helper.name = name; + return helper; + } } """)); @@ -765,19 +808,32 @@ void resolverUsageScope_PrefillsCoveredFieldsWhenMajorityMatches() { """ package lib; public class LibHelper { - public String getName() { return ""; } - public boolean isActive() { return true; } - public int getAge() { return 0; } + private final String name; + private final boolean active; + private final int age; + public LibHelper(String name, boolean active, int age) { + this.name = name; + this.active = active; + this.age = age; + } + public String getName() { return name; } + public boolean isActive() { return active; } + public int getAge() { return age; } } """), ProcessorTestUtils.forSource( """ package lib; public class LibHelperBuilder { + private String name; + private boolean active; public LibHelperBuilder() {} - public LibHelperBuilder name(String name) { return this; } - public LibHelperBuilder active(boolean active) { return this; } - public LibHelper build() { return new LibHelper(); } + public LibHelperBuilder name(String name) { this.name = name; return this; } + public LibHelperBuilder active(boolean active) { + this.active = active; + return this; + } + public LibHelper build() { return new LibHelper(name, active, 0); } } """));