From 72dd9d67b1bceab5f3bf8e53d6fa5a3ed950cb16 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 18:53:13 +0100 Subject: [PATCH 1/9] Adding a test and implementation for annotations on types --- .../builders/processor/dtos/TypeName.java | 21 ++ .../processor/dtos/TypeNamePrimitive.java | 2 +- .../util/FieldAnnotationExtractor.java | 24 ++ .../processor/util/JavaLangMapper.java | 133 +++++---- .../processor/util/JavapoetMapper.java | 63 ++-- .../processor/TypeUseAnnotationTest.java | 281 ++++++++++++++++++ 6 files changed, 439 insertions(+), 85 deletions(-) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java index 01e772e4..e4c7540a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java @@ -39,6 +39,9 @@ public class TypeName { /** Name of class. */ private final String className; + /** Annotations on this type (TYPE_USE). */ + private final java.util.List annotations = new java.util.ArrayList<>(); + /** * Constructor for TypeName. * @@ -69,6 +72,24 @@ public String getClassName() { return className; } + /** + * Returns the list of annotations on this type. + * + * @return list of annotations + */ + public java.util.List getAnnotations() { + return annotations; + } + + /** + * Adds an annotation to this type. + * + * @param annotation the annotation to add + */ + public void addAnnotation(AnnotationDto annotation) { + this.annotations.add(annotation); + } + /** * Helper function to hold a specific inner type.Is empty if this is a class without generic parts * or a class has multiple generics. diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNamePrimitive.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNamePrimitive.java index 2e109867..4cc58add 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNamePrimitive.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNamePrimitive.java @@ -65,7 +65,7 @@ public class TypeNamePrimitive extends TypeName { * * @param primitiveType primitive enum type */ - protected TypeNamePrimitive(PrimitiveTypeEnum primitiveType) { + public TypeNamePrimitive(PrimitiveTypeEnum primitiveType) { super("", primitiveType.name().toLowerCase()); this.type = primitiveType; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java index 18fd3a1d..777cd2c7 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java @@ -91,6 +91,30 @@ public static List extractAnnotations( return annotations; } + /** + * Extracts annotations from a type mirror. Filters out annotations that should not be copied to + * the builder. + * + * @param typeMirror the type mirror containing annotations + * @param context processing context + * @return list of annotations to be copied to the builder field + */ + public static List extractAnnotations( + javax.lang.model.type.TypeMirror typeMirror, ProcessingContext context) { + List annotations = new ArrayList<>(); + List annotationMirrors = typeMirror.getAnnotationMirrors(); + + context.debug( + " -> Extracting %d annotation(s) from type %s", + annotationMirrors.size(), typeMirror.toString()); + + for (AnnotationMirror mirror : annotationMirrors) { + extractAnnotation(mirror, context).ifPresent(annotations::add); + } + + return annotations; + } + /** * Extracts a single annotation from an AnnotationMirror. Filters out annotations that should not * be copied to the builder. diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java index bf265a70..525e4ea6 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java @@ -394,65 +394,78 @@ private static TypeMirror findSupertype( } private static TypeName extractType(TypeMirror typeOfParameter, ProcessingContext context) { - return typeOfParameter.accept( - new SimpleTypeVisitor14() { - - @Override - public TypeName visitPrimitive(PrimitiveType t, Void p) { - return switch (t.getKind()) { - case BOOLEAN -> TypeNamePrimitive.BOOLEAN; - case BYTE -> TypeNamePrimitive.BYTE; - case SHORT -> TypeNamePrimitive.SHORT; - case INT -> TypeNamePrimitive.INT; - case LONG -> TypeNamePrimitive.LONG; - case CHAR -> TypeNamePrimitive.CHAR; - case FLOAT -> TypeNamePrimitive.FLOAT; - case DOUBLE -> TypeNamePrimitive.DOUBLE; - default -> throw new IllegalStateException("Unsupported Primitive type"); - }; - } - - @Override - public TypeName visitDeclared(DeclaredType t, Void p) { - TypeElement elementOfParameter = (TypeElement) context.asElement(typeOfParameter); - String simpleClassName = elementOfParameter.getSimpleName().toString(); - String packageName = context.getPackageName(elementOfParameter); - TypeName rawType = new TypeName(packageName, simpleClassName); - TypeMirror enclosingType = t.getEnclosingType(); - TypeName enclosing = - (enclosingType.getKind() != NONE) - && !t.asElement().getModifiers().contains(Modifier.STATIC) - ? enclosingType.accept(this, null) - : null; - if (t.getTypeArguments().isEmpty() && !(enclosing instanceof TypeNameGeneric)) { - return wrapInCollectionTypeIfApplicable(rawType, List.of(), typeOfParameter, context); - } - - List typesExtracted = new ArrayList<>(t.getTypeArguments()); - if (typesExtracted.isEmpty()) { - return wrapInCollectionTypeIfApplicable(rawType, List.of(), typeOfParameter, context); - } else { - List argTypes = extractTypeForList(typesExtracted, context); - return wrapInCollectionTypeIfApplicable(rawType, argTypes, typeOfParameter, context); - } - } - - @Override - public TypeNameArray visitArray(ArrayType t, Void p) { - return new TypeNameArray(extractType(t.getComponentType(), context)); - } - - @Override - public TypeName visitTypeVariable(TypeVariable t, Void p) { - String name = t.asElement().getSimpleName().toString(); - return new TypeNameVariable(name); - } - - @Override - protected TypeName defaultAction(TypeMirror e, Void p) { - throw new IllegalArgumentException("Unexpected type mirror: " + e); - } - }, - null); + TypeName typeName = + typeOfParameter.accept( + new SimpleTypeVisitor14() { + + @Override + public TypeName visitPrimitive(PrimitiveType t, Void p) { + return switch (t.getKind()) { + case BOOLEAN -> + new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.BOOLEAN); + case BYTE -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.BYTE); + case SHORT -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.SHORT); + case INT -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.INT); + case LONG -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.LONG); + case CHAR -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.CHAR); + case FLOAT -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.FLOAT); + case DOUBLE -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.DOUBLE); + default -> throw new IllegalStateException("Unsupported Primitive type"); + }; + } + + @Override + public TypeName visitDeclared(DeclaredType t, Void p) { + TypeElement elementOfParameter = (TypeElement) context.asElement(typeOfParameter); + String simpleClassName = elementOfParameter.getSimpleName().toString(); + String packageName = context.getPackageName(elementOfParameter); + TypeName rawType = new TypeName(packageName, simpleClassName); + TypeMirror enclosingType = t.getEnclosingType(); + TypeName enclosing = + (enclosingType.getKind() != NONE) + && !t.asElement().getModifiers().contains(Modifier.STATIC) + ? enclosingType.accept(this, null) + : null; + if (t.getTypeArguments().isEmpty() && !(enclosing instanceof TypeNameGeneric)) { + return wrapInCollectionTypeIfApplicable( + rawType, List.of(), typeOfParameter, context); + } + + List typesExtracted = new ArrayList<>(t.getTypeArguments()); + if (typesExtracted.isEmpty()) { + return wrapInCollectionTypeIfApplicable( + rawType, List.of(), typeOfParameter, context); + } else { + List argTypes = extractTypeForList(typesExtracted, context); + return wrapInCollectionTypeIfApplicable( + rawType, argTypes, typeOfParameter, context); + } + } + + @Override + public TypeNameArray visitArray(ArrayType t, Void p) { + return new TypeNameArray(extractType(t.getComponentType(), context)); + } + + @Override + public TypeName visitTypeVariable(TypeVariable t, Void p) { + String name = t.asElement().getSimpleName().toString(); + return new TypeNameVariable(name); + } + + @Override + protected TypeName defaultAction(TypeMirror e, Void p) { + throw new IllegalArgumentException("Unexpected type mirror: " + e); + } + }, + null); + + if (typeName != null) { + List annotations = + FieldAnnotationExtractor.extractAnnotations(typeOfParameter, context); + annotations.forEach(typeName::addAnnotation); + } + + return typeName; } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java index 7a407497..e30ecfb8 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java @@ -72,36 +72,51 @@ public static TypeName[] map2TypeArgumentsArray( */ public static TypeName map2ParameterType( org.javahelpers.simple.builders.processor.dtos.TypeName parameterType) { + TypeName typeName = null; + // Handle generic type variables (e.g., T, K, V) if (parameterType instanceof org.javahelpers.simple.builders.processor.dtos.TypeNameVariable typeVariable) { - return TypeVariableName.get(typeVariable.getClassName()); + typeName = TypeVariableName.get(typeVariable.getClassName()); } - ClassName classNameParameter = - ClassName.get(parameterType.getPackageName(), parameterType.getClassName()); - if (parameterType instanceof TypeNameArray parameterTypeArray) { - return ArrayTypeName.of(map2ParameterType(parameterTypeArray.getTypeOfArray())); - } else if (parameterType instanceof TypeNamePrimitive parameterTypePrim) { - return switch (parameterTypePrim.getType()) { - case BOOLEAN -> TypeName.BOOLEAN; - case BYTE -> TypeName.BYTE; - case CHAR -> TypeName.CHAR; - case DOUBLE -> TypeName.DOUBLE; - case FLOAT -> TypeName.FLOAT; - case INT -> TypeName.INT; - case LONG -> TypeName.LONG; - case SHORT -> TypeName.SHORT; - default -> null; - }; - } else if (parameterType instanceof TypeNameGeneric param) { - // Handle raw types (e.g., List without ) - return just the class name - if (param.getInnerTypeArguments().isEmpty()) { - return classNameParameter; + // Handle primitives BEFORE calling ClassName.get() to avoid IllegalArgumentException in + // JavaPoet 0.9.0+ + else if (parameterType instanceof TypeNamePrimitive parameterTypePrim) { + typeName = + switch (parameterTypePrim.getType()) { + case BOOLEAN -> TypeName.BOOLEAN; + case BYTE -> TypeName.BYTE; + case CHAR -> TypeName.CHAR; + case DOUBLE -> TypeName.DOUBLE; + case FLOAT -> TypeName.FLOAT; + case INT -> TypeName.INT; + case LONG -> TypeName.LONG; + case SHORT -> TypeName.SHORT; + default -> null; + }; + } else if (parameterType instanceof TypeNameArray parameterTypeArray) { + typeName = ArrayTypeName.of(map2ParameterType(parameterTypeArray.getTypeOfArray())); + } else { + // Now safe to call ClassName.get() for non-primitive types + ClassName classNameParameter = + ClassName.get(parameterType.getPackageName(), parameterType.getClassName()); + if (parameterType instanceof TypeNameGeneric param) { + // Handle raw types (e.g., List without ) - return just the class name + if (param.getInnerTypeArguments().isEmpty()) { + typeName = classNameParameter; + } else { + TypeName[] typeArgs = map2TypeArgumentsArray(param.getInnerTypeArguments()); + typeName = ParameterizedTypeName.get(classNameParameter, typeArgs); + } + } else { + typeName = classNameParameter; } - TypeName[] typeArgs = map2TypeArgumentsArray(param.getInnerTypeArguments()); - return ParameterizedTypeName.get(classNameParameter, typeArgs); } - return classNameParameter; + + if (typeName != null && !parameterType.getAnnotations().isEmpty()) { + return typeName.annotated(map2AnnotationSpecs(parameterType.getAnnotations())); + } + return typeName; } /** diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java new file mode 100644 index 00000000..f4db6b87 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java @@ -0,0 +1,281 @@ +package org.javahelpers.simple.builders.processor; + +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.contains; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.createCompiler; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.createMockAnnotation; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.printDiagnosticsOnVerbose; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; +import org.junit.jupiter.api.Test; + +/** + * Test that TYPE_USE annotations are properly handled in generated builders. + * + *

TYPE_USE annotations apply to type usage, not declarations: + * + *

+ * List<@NotNull String> items;
+ * 
+ * + *

These annotations should be preserved on: + * + *

    + *
  • Builder field member variables (TrackedValue fields) + *
  • Method parameters in setter methods + *
  • Method parameters in helper methods (add2FieldName, etc.) + *
  • Return types where applicable + *
+ * + * @see Issue #92 + */ +class TypeUseAnnotationTest { + + private Compilation compileSources(JavaFileObject... sources) { + Compilation compilation = createCompiler().compile(sources); + printDiagnosticsOnVerbose(compilation); + return compilation; + } + + @Test + void typeUseAnnotations_onListElements_copiedToBuilderMethods() { + String packageName = "test.typeuse.list"; + + JavaFileObject notNullAnnotation = + createMockAnnotation(packageName + ".annotations", "NotNull", "ElementType.TYPE_USE"); + + JavaFileObject person = + JavaFileObjects.forSourceString( + packageName + ".Person", + """ + package test.typeuse.list; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import test.typeuse.list.annotations.NotNull; + import java.util.List; + + @SimpleBuilder + public class Person { + private final List<@NotNull String> nicknames; + + public Person(List<@NotNull String> nicknames) { + this.nicknames = nicknames; + } + + public List<@NotNull String> getNicknames() { return nicknames; } + } + """); + + Compilation compilation = compileSources(notNullAnnotation, person); + String generatedCode = loadGeneratedSource(compilation, "PersonBuilder"); + ProcessorAsserts.assertGenerationSucceeded(compilation, "PersonBuilder", generatedCode); + + System.out.println("=== Generated PersonBuilder with TYPE_USE annotations ==="); + System.out.println(generatedCode); + System.out.println("=== End of Generated Code ==="); + + // Verify that TYPE_USE annotations are preserved in builder field + ProcessorAsserts.assertingResult( + generatedCode, + // Builder field should have TYPE_USE annotation + contains("TrackedValue> nicknames"), + // Setter method parameter should have TYPE_USE annotation + contains("nicknames(List<@NotNull String> nicknames)"), + // VarArgs method parameter should have TYPE_USE annotation + contains("nicknames(@NotNull String... nicknames)"), + // add2Nicknames method parameter should have TYPE_USE annotation + contains("add2Nicknames(@NotNull String element)")); + } + + @Test + void typeUseAnnotations_onSetElements_copiedToBuilderMethods() { + String packageName = "test.typeuse.set"; + + JavaFileObject notNullAnnotation = + createMockAnnotation(packageName + ".annotations", "NotNull", "ElementType.TYPE_USE"); + + JavaFileObject product = + JavaFileObjects.forSourceString( + packageName + ".Product", + """ + package test.typeuse.set; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import test.typeuse.set.annotations.NotNull; + import java.util.Set; + + @SimpleBuilder + public class Product { + private final Set<@NotNull String> tags; + + public Product(Set<@NotNull String> tags) { + this.tags = tags; + } + + public Set<@NotNull String> getTags() { return tags; } + } + """); + + Compilation compilation = compileSources(notNullAnnotation, product); + String generatedCode = loadGeneratedSource(compilation, "ProductBuilder"); + ProcessorAsserts.assertGenerationSucceeded(compilation, "ProductBuilder", generatedCode); + + System.out.println("=== Generated ProductBuilder with TYPE_USE annotations on Set ==="); + System.out.println(generatedCode); + System.out.println("=== End of Generated Code ==="); + + // Verify that TYPE_USE annotations are preserved + ProcessorAsserts.assertingResult( + generatedCode, + // Builder field should have TYPE_USE annotation + contains("TrackedValue> tags"), + // Setter method parameter should have TYPE_USE annotation + contains("tags(Set<@NotNull String> tags)"), + // VarArgs method parameter should have TYPE_USE annotation + contains("tags(@NotNull String... tags)"), + // add2Tags method parameter should have TYPE_USE annotation + contains("add2Tags(@NotNull String element)")); + } + + @Test + void typeUseAnnotations_onMapValues_copiedToBuilderMethods() { + String packageName = "test.typeuse.map"; + + JavaFileObject notNullAnnotation = + createMockAnnotation(packageName + ".annotations", "NotNull", "ElementType.TYPE_USE"); + + JavaFileObject config = + JavaFileObjects.forSourceString( + packageName + ".Config", + """ + package test.typeuse.map; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import test.typeuse.map.annotations.NotNull; + import java.util.Map; + + @SimpleBuilder + public class Config { + private final Map properties; + + public Config(Map properties) { + this.properties = properties; + } + + public Map getProperties() { return properties; } + } + """); + + Compilation compilation = compileSources(notNullAnnotation, config); + String generatedCode = loadGeneratedSource(compilation, "ConfigBuilder"); + ProcessorAsserts.assertGenerationSucceeded(compilation, "ConfigBuilder", generatedCode); + + System.out.println("=== Generated ConfigBuilder with TYPE_USE annotations on Map ==="); + System.out.println(generatedCode); + System.out.println("=== End of Generated Code ==="); + + // Verify that TYPE_USE annotations are preserved + ProcessorAsserts.assertingResult( + generatedCode, + // Builder field should have TYPE_USE annotation + contains("TrackedValue> properties"), + // Setter method parameter should have TYPE_USE annotation + contains("properties(Map properties)")); + } + + @Test + void typeUseAnnotations_multipleOnSameType_allCopied() { + String packageName = "test.typeuse.multiple"; + + JavaFileObject notNullAnnotation = + createMockAnnotation(packageName + ".annotations", "NotNull", "ElementType.TYPE_USE"); + + JavaFileObject validAnnotation = + createMockAnnotation(packageName + ".annotations", "Valid", "ElementType.TYPE_USE"); + + JavaFileObject data = + JavaFileObjects.forSourceString( + packageName + ".Data", + """ + package test.typeuse.multiple; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import test.typeuse.multiple.annotations.NotNull; + import test.typeuse.multiple.annotations.Valid; + import java.util.List; + + @SimpleBuilder + public class Data { + private final List<@NotNull @Valid String> values; + + public Data(List<@NotNull @Valid String> values) { + this.values = values; + } + + public List<@NotNull @Valid String> getValues() { return values; } + } + """); + + Compilation compilation = compileSources(notNullAnnotation, validAnnotation, data); + String generatedCode = loadGeneratedSource(compilation, "DataBuilder"); + ProcessorAsserts.assertGenerationSucceeded(compilation, "DataBuilder", generatedCode); + + System.out.println("=== Generated DataBuilder with multiple TYPE_USE annotations ==="); + System.out.println(generatedCode); + System.out.println("=== End of Generated Code ==="); + + // Verify that both TYPE_USE annotations are preserved + ProcessorAsserts.assertingResult( + generatedCode, + // Builder field should have both TYPE_USE annotations + contains("TrackedValue> values"), + // Method parameters should have both annotations + contains("values(@NotNull @Valid String... values)"), + contains("add2Values(@NotNull @Valid String element)")); + } + + @Test + void typeUseAnnotations_onNestedGenerics_copiedCorrectly() { + String packageName = "test.typeuse.nested"; + + JavaFileObject notNullAnnotation = + createMockAnnotation(packageName + ".annotations", "NotNull", "ElementType.TYPE_USE"); + + JavaFileObject container = + JavaFileObjects.forSourceString( + packageName + ".Container", + """ + package test.typeuse.nested; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import test.typeuse.nested.annotations.NotNull; + import java.util.List; + import java.util.Map; + + @SimpleBuilder + public class Container { + private final List> data; + + public Container(List> data) { + this.data = data; + } + + public List> getData() { return data; } + } + """); + + Compilation compilation = compileSources(notNullAnnotation, container); + String generatedCode = loadGeneratedSource(compilation, "ContainerBuilder"); + ProcessorAsserts.assertGenerationSucceeded(compilation, "ContainerBuilder", generatedCode); + + System.out.println("=== Generated ContainerBuilder with nested TYPE_USE annotations ==="); + System.out.println(generatedCode); + System.out.println("=== End of Generated Code ==="); + + // Verify that TYPE_USE annotations are preserved in nested generics + ProcessorAsserts.assertingResult( + generatedCode, + // Builder field should have TYPE_USE annotation in nested position + contains("TrackedValue>> data"), + // Setter method parameter should have TYPE_USE annotation + contains("data(List> data)")); + } +} From 2030943f7f7bef160a74fe3e9e69ffa356e86ba8 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 19:00:43 +0100 Subject: [PATCH 2/9] Refactoring of map2ParameterType to decrease complexity --- .../processor/util/JavapoetMapper.java | 64 +++++++++---------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java index e30ecfb8..f1010e96 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java @@ -72,53 +72,49 @@ public static TypeName[] map2TypeArgumentsArray( */ public static TypeName map2ParameterType( org.javahelpers.simple.builders.processor.dtos.TypeName parameterType) { - TypeName typeName = null; - - // Handle generic type variables (e.g., T, K, V) + TypeName typeName; if (parameterType instanceof org.javahelpers.simple.builders.processor.dtos.TypeNameVariable typeVariable) { typeName = TypeVariableName.get(typeVariable.getClassName()); - } - // Handle primitives BEFORE calling ClassName.get() to avoid IllegalArgumentException in - // JavaPoet 0.9.0+ - else if (parameterType instanceof TypeNamePrimitive parameterTypePrim) { - typeName = - switch (parameterTypePrim.getType()) { - case BOOLEAN -> TypeName.BOOLEAN; - case BYTE -> TypeName.BYTE; - case CHAR -> TypeName.CHAR; - case DOUBLE -> TypeName.DOUBLE; - case FLOAT -> TypeName.FLOAT; - case INT -> TypeName.INT; - case LONG -> TypeName.LONG; - case SHORT -> TypeName.SHORT; - default -> null; - }; + } else if (parameterType instanceof TypeNamePrimitive parameterTypePrim) { + typeName = mapPrimitive(parameterTypePrim); } else if (parameterType instanceof TypeNameArray parameterTypeArray) { typeName = ArrayTypeName.of(map2ParameterType(parameterTypeArray.getTypeOfArray())); + } else if (parameterType instanceof TypeNameGeneric parameterTypeGeneric) { + typeName = mapGeneric(parameterTypeGeneric); } else { - // Now safe to call ClassName.get() for non-primitive types - ClassName classNameParameter = - ClassName.get(parameterType.getPackageName(), parameterType.getClassName()); - if (parameterType instanceof TypeNameGeneric param) { - // Handle raw types (e.g., List without ) - return just the class name - if (param.getInnerTypeArguments().isEmpty()) { - typeName = classNameParameter; - } else { - TypeName[] typeArgs = map2TypeArgumentsArray(param.getInnerTypeArguments()); - typeName = ParameterizedTypeName.get(classNameParameter, typeArgs); - } - } else { - typeName = classNameParameter; - } + typeName = ClassName.get(parameterType.getPackageName(), parameterType.getClassName()); } if (typeName != null && !parameterType.getAnnotations().isEmpty()) { - return typeName.annotated(map2AnnotationSpecs(parameterType.getAnnotations())); + typeName = typeName.annotated(map2AnnotationSpecs(parameterType.getAnnotations())); } return typeName; } + private static TypeName mapPrimitive(TypeNamePrimitive parameterTypePrim) { + return switch (parameterTypePrim.getType()) { + case BOOLEAN -> TypeName.BOOLEAN; + case BYTE -> TypeName.BYTE; + case CHAR -> TypeName.CHAR; + case DOUBLE -> TypeName.DOUBLE; + case FLOAT -> TypeName.FLOAT; + case INT -> TypeName.INT; + case LONG -> TypeName.LONG; + case SHORT -> TypeName.SHORT; + default -> null; + }; + } + + private static TypeName mapGeneric(TypeNameGeneric param) { + ClassName classNameParameter = ClassName.get(param.getPackageName(), param.getClassName()); + if (param.getInnerTypeArguments().isEmpty()) { + return classNameParameter; + } + TypeName[] typeArgs = map2TypeArgumentsArray(param.getInnerTypeArguments()); + return ParameterizedTypeName.get(classNameParameter, typeArgs); + } + /** * Mapper for typename. Maps into javapoet classes. * From fe6fa0965c5bd2ed54e143d7d3778d233a771527 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 19:16:14 +0100 Subject: [PATCH 3/9] Improving tests for TypeUseAnnotation --- .../processor/TypeUseAnnotationTest.java | 36 +++++-------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java index f4db6b87..33556477 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java @@ -72,10 +72,6 @@ public Person(List<@NotNull String> nicknames) { String generatedCode = loadGeneratedSource(compilation, "PersonBuilder"); ProcessorAsserts.assertGenerationSucceeded(compilation, "PersonBuilder", generatedCode); - System.out.println("=== Generated PersonBuilder with TYPE_USE annotations ==="); - System.out.println(generatedCode); - System.out.println("=== End of Generated Code ==="); - // Verify that TYPE_USE annotations are preserved in builder field ProcessorAsserts.assertingResult( generatedCode, @@ -121,10 +117,6 @@ public Product(Set<@NotNull String> tags) { String generatedCode = loadGeneratedSource(compilation, "ProductBuilder"); ProcessorAsserts.assertGenerationSucceeded(compilation, "ProductBuilder", generatedCode); - System.out.println("=== Generated ProductBuilder with TYPE_USE annotations on Set ==="); - System.out.println(generatedCode); - System.out.println("=== End of Generated Code ==="); - // Verify that TYPE_USE annotations are preserved ProcessorAsserts.assertingResult( generatedCode, @@ -170,10 +162,6 @@ public Config(Map properties) { String generatedCode = loadGeneratedSource(compilation, "ConfigBuilder"); ProcessorAsserts.assertGenerationSucceeded(compilation, "ConfigBuilder", generatedCode); - System.out.println("=== Generated ConfigBuilder with TYPE_USE annotations on Map ==="); - System.out.println(generatedCode); - System.out.println("=== End of Generated Code ==="); - // Verify that TYPE_USE annotations are preserved ProcessorAsserts.assertingResult( generatedCode, @@ -191,7 +179,7 @@ void typeUseAnnotations_multipleOnSameType_allCopied() { createMockAnnotation(packageName + ".annotations", "NotNull", "ElementType.TYPE_USE"); JavaFileObject validAnnotation = - createMockAnnotation(packageName + ".annotations", "Valid", "ElementType.TYPE_USE"); + createMockAnnotation(packageName + ".annotations", "Validated", "ElementType.TYPE_USE"); JavaFileObject data = JavaFileObjects.forSourceString( @@ -200,18 +188,18 @@ void typeUseAnnotations_multipleOnSameType_allCopied() { package test.typeuse.multiple; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import test.typeuse.multiple.annotations.NotNull; - import test.typeuse.multiple.annotations.Valid; + import test.typeuse.multiple.annotations.Validated; import java.util.List; @SimpleBuilder public class Data { - private final List<@NotNull @Valid String> values; + private final List<@NotNull @Validated String> values; - public Data(List<@NotNull @Valid String> values) { + public Data(List<@NotNull @Validated String> values) { this.values = values; } - public List<@NotNull @Valid String> getValues() { return values; } + public List<@NotNull @Validated String> getValues() { return values; } } """); @@ -219,18 +207,14 @@ public Data(List<@NotNull @Valid String> values) { String generatedCode = loadGeneratedSource(compilation, "DataBuilder"); ProcessorAsserts.assertGenerationSucceeded(compilation, "DataBuilder", generatedCode); - System.out.println("=== Generated DataBuilder with multiple TYPE_USE annotations ==="); - System.out.println(generatedCode); - System.out.println("=== End of Generated Code ==="); - // Verify that both TYPE_USE annotations are preserved ProcessorAsserts.assertingResult( generatedCode, // Builder field should have both TYPE_USE annotations - contains("TrackedValue> values"), + contains("TrackedValue> values"), // Method parameters should have both annotations - contains("values(@NotNull @Valid String... values)"), - contains("add2Values(@NotNull @Valid String element)")); + contains("values(@NotNull @Validated String... values)"), + contains("add2Values(@NotNull @Validated String element)")); } @Test @@ -266,10 +250,6 @@ public Container(List> data) { String generatedCode = loadGeneratedSource(compilation, "ContainerBuilder"); ProcessorAsserts.assertGenerationSucceeded(compilation, "ContainerBuilder", generatedCode); - System.out.println("=== Generated ContainerBuilder with nested TYPE_USE annotations ==="); - System.out.println(generatedCode); - System.out.println("=== End of Generated Code ==="); - // Verify that TYPE_USE annotations are preserved in nested generics ProcessorAsserts.assertingResult( generatedCode, From aa117836f43ede82eb86b09079099e8d84da3cd1 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 19:33:10 +0100 Subject: [PATCH 4/9] Adding a test for non-typeUseAnnotations being not taking over --- .../processor/TypeUseAnnotationTest.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java index 33556477..47a30e1d 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java @@ -1,6 +1,7 @@ package org.javahelpers.simple.builders.processor; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.contains; +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.notContains; import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.createCompiler; import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.createMockAnnotation; import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; @@ -258,4 +259,71 @@ public Container(List> data) { // Setter method parameter should have TYPE_USE annotation contains("data(List> data)")); } + + @Test + void nonTypeUseAnnotations_shouldNotBeCopiedToTypeUseLocations() { + String packageName = "test.target.param"; + + // Create an annotation that targets ONLY parameters, NOT types + JavaFileObject paramAnnotation = + createMockAnnotation(packageName + ".annotations", "ParamOnly", "ElementType.PARAMETER"); + + JavaFileObject typeUseAnnotation = + createMockAnnotation(packageName + ".annotations", "TypeUse", "ElementType.TYPE_USE"); + + JavaFileObject data = + JavaFileObjects.forSourceString( + packageName + ".Data", + """ + package test.target.param; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import test.target.param.annotations.ParamOnly; + import test.target.param.annotations.TypeUse; + import java.util.List; + + @SimpleBuilder + public class Data { + // @ParamOnly is invalid on the type itself in Java, but could appear + // on the parameter declaration of the constructor/setter we analyze. + // However, if we parse it from a parameter and then try to apply it to a type + // in the builder (e.g. List<@ParamOnly String>), that would be invalid code. + + private final String value; + private final List list; + + // The annotation is on the parameter declaration, NOT on the type + public Data(@ParamOnly String value, @TypeUse List list) { + this.value = value; + this.list = list; + } + + public String getValue() { return value; } + public List getList() { return list; } + } + """); + + Compilation compilation = compileSources(paramAnnotation, typeUseAnnotation, data); + String generatedCode = loadGeneratedSource(compilation, "DataBuilder"); + ProcessorAsserts.assertGenerationSucceeded(compilation, "DataBuilder", generatedCode); + + // The @ParamOnly annotation comes from the constructor parameter. + // It should NOT appear on the field type in the builder. + ProcessorAsserts.assertingResult( + generatedCode, + // Should NOT see @ParamOnly on the field type + notContains("TrackedValue<@ParamOnly String> value"), + notContains("TrackedValue> list"), + + // Should see @TypeUse (if it was on the type use) + // Note: In the source above, @TypeUse is on the parameter, but we want to check + // if the processor distinguishes based on the annotation's allowed targets. + // Actually, 'extractAnnotations' reads from the Element (Parameter), + // so it sees @ParamOnly. + + // Ideally, we want the builder field to be clean of @ParamOnly + contains("TrackedValue value"), + + // Verify that the @TypeUse annotation IS present on the list field + contains("TrackedValue<@TypeUse List> list")); + } } From 6fe7b6c595c7b18ab830df216c32d25a0c3b5887 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 21:23:53 +0100 Subject: [PATCH 5/9] Adding a new configuration for possibility to deactivate generation of TypeUse annotations in builders --- .../core/annotations/SimpleBuilder.java | 10 ++++++++ docs/CONFIGURATION.md | 20 +++++++++++++++ .../processor/dtos/BuilderConfiguration.java | 25 +++++++++++++++++++ .../enums/CompilerArgumentsEnum.java | 3 +++ .../util/BuilderConfigurationReader.java | 1 + .../util/CompilerArgumentsReader.java | 1 + .../CompilerArgumentsReaderTest.java | 2 ++ .../ConfigurationProcessingTest.java | 3 +++ 8 files changed, 65 insertions(+) diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java index 2667deab..78c30449 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 @@ -331,6 +331,16 @@ */ OptionState generateUnboxedOptional() default OptionState.UNSET; + /** + * Copy type annotations from the DTO fields to the builder fields/methods.
+ * Useful for validation annotations (e.g. @NotNull, @Size) or other metadata that should be + * preserved. + * + *

Default: ENABLED
+ * Compiler option: -Asimplebuilder.copyTypeAnnotations + */ + OptionState copyTypeAnnotations() default OptionState.UNSET; + /** * Generate helper methods with a ArrayListBuilder supplier for lists instead of simple * supplier, which would not allow to use in a chanined way:
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 16be437a..103166a9 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -461,6 +461,26 @@ PersonDto person = PersonDtoBuilder.create() --- +#### `copyTypeAnnotations` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.copyTypeAnnotations=ENABLED|DISABLED` + +Copies type annotations (TYPE_USE) from the DTO fields to the builder fields and methods. This is useful for validation annotations (e.g. `@NotNull`, `@Size`) or other metadata that should be preserved. + +**When ENABLED**: +```java +// DTO +private List<@NotNull String> items; + +// Generated Builder +private TrackedValue> items; +public Builder items(List<@NotNull String> items) { ... } +``` + +**When DISABLED**: Type annotations are stripped from the builder. + +--- + ### Collection Helpers #### `usingArrayListBuilder` diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 8ebb91a8..3f3f4b93 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -76,6 +76,7 @@ public record BuilderConfiguration( OptionState generateStringFormatHelpers, OptionState generateAddToCollectionHelpers, OptionState generateUnboxedOptional, + OptionState copyTypeAnnotations, OptionState usingArrayListBuilder, OptionState usingArrayListBuilderWithElementBuilders, OptionState usingHashSetBuilder, @@ -101,6 +102,7 @@ public record BuilderConfiguration( .generateStringFormatHelpers(ENABLED) .generateAddToCollectionHelpers(ENABLED) .generateUnboxedOptional(ENABLED) + .copyTypeAnnotations(ENABLED) .usingArrayListBuilder(ENABLED) .usingArrayListBuilderWithElementBuilders(ENABLED) .usingHashSetBuilder(ENABLED) @@ -171,6 +173,10 @@ public boolean shouldGenerateUnboxedOptional() { return generateUnboxedOptional == ENABLED; } + public boolean shouldCopyTypeAnnotations() { + return copyTypeAnnotations == ENABLED; + } + public boolean shouldUseGeneratedAnnotation() { return usingGeneratedAnnotation == ENABLED; } @@ -239,6 +245,7 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.generateAddToCollectionHelpers, this.generateAddToCollectionHelpers)) .generateUnboxedOptional( mergeOptionState(other.generateUnboxedOptional, this.generateUnboxedOptional)) + .copyTypeAnnotations(mergeOptionState(other.copyTypeAnnotations, this.copyTypeAnnotations)) .usingArrayListBuilder( mergeOptionState(other.usingArrayListBuilder, this.usingArrayListBuilder)) .usingArrayListBuilderWithElementBuilders( @@ -328,6 +335,12 @@ public String toString() { if (generateVarArgsHelpers != UNSET) { builder.append("generateVarArgsHelpers", generateVarArgsHelpers); } + if (generateUnboxedOptional != UNSET) { + builder.append("generateUnboxedOptional", generateUnboxedOptional); + } + if (copyTypeAnnotations != UNSET) { + builder.append("copyTypeAnnotations", copyTypeAnnotations); + } if (usingArrayListBuilder != UNSET) { builder.append("usingArrayListBuilder", usingArrayListBuilder); } @@ -379,6 +392,7 @@ public static class Builder { private OptionState generateStringFormatHelpers = OptionState.UNSET; private OptionState generateAddToCollectionHelpers = OptionState.UNSET; private OptionState generateUnboxedOptional = OptionState.UNSET; + private OptionState copyTypeAnnotations = OptionState.UNSET; private OptionState usingArrayListBuilder = OptionState.UNSET; private OptionState usingArrayListBuilderWithElementBuilders = OptionState.UNSET; private OptionState usingHashSetBuilder = OptionState.UNSET; @@ -488,6 +502,16 @@ public Builder generateUnboxedOptional(boolean value) { return this; } + public Builder copyTypeAnnotations(OptionState value) { + this.copyTypeAnnotations = value; + return this; + } + + public Builder copyTypeAnnotations(boolean value) { + this.copyTypeAnnotations = value ? ENABLED : DISABLED; + return this; + } + public Builder usingArrayListBuilder(OptionState value) { this.usingArrayListBuilder = value; return this; @@ -621,6 +645,7 @@ public BuilderConfiguration build() { generateStringFormatHelpers, generateAddToCollectionHelpers, generateUnboxedOptional, + copyTypeAnnotations, usingArrayListBuilder, usingArrayListBuilderWithElementBuilders, usingHashSetBuilder, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index 77f8b1b4..61d93b32 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -75,6 +75,9 @@ public enum CompilerArgumentsEnum { /** Option for unboxed optional generation. */ GENERATE_UNBOXED_OPTIONAL("generateUnboxedOptional"), + /** Option for copying type annotations. */ + COPY_TYPE_ANNOTATIONS("copyTypeAnnotations"), + /** Option for ArrayList builder usage. */ USING_ARRAY_LIST_BUILDER("usingArrayListBuilder"), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index fa94c6c7..70b113e9 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -175,6 +175,7 @@ private BuilderConfiguration parseOptionsFromMirror(AnnotationMirror optionsMirr builder.generateAddToCollectionHelpers(OptionState.valueOf(enumValue)); case "generateUnboxedOptional" -> builder.generateUnboxedOptional(OptionState.valueOf(enumValue)); + case "copyTypeAnnotations" -> builder.copyTypeAnnotations(OptionState.valueOf(enumValue)); case "usingArrayListBuilder" -> builder.usingArrayListBuilder(OptionState.valueOf(enumValue)); case "usingArrayListBuilderWithElementBuilders" -> diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index 10497705..585883f4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -156,6 +156,7 @@ public BuilderConfiguration readBuilderConfiguration() { .generateAddToCollectionHelpers( readOptionState(CompilerArgumentsEnum.GENERATE_ADD_TO_COLLECTION_HELPERS)) .generateUnboxedOptional(readOptionState(CompilerArgumentsEnum.GENERATE_UNBOXED_OPTIONAL)) + .copyTypeAnnotations(readOptionState(CompilerArgumentsEnum.COPY_TYPE_ANNOTATIONS)) .usingArrayListBuilder(readOptionState(CompilerArgumentsEnum.USING_ARRAY_LIST_BUILDER)) .usingArrayListBuilderWithElementBuilders( readOptionState(CompilerArgumentsEnum.USING_ARRAY_LIST_BUILDER_WITH_ELEMENT_BUILDERS)) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java index 0edef695..b0d68f97 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java @@ -350,6 +350,7 @@ void readBuilderConfiguration_AllOptionsSet_ReadsCorrectly() { .put("simplebuilder.builderConstructorAccess", "private") .put("simplebuilder.methodAccess", "package-private") .put("simplebuilder.generateVarArgsHelpers", "disabled") + .put("simplebuilder.copyTypeAnnotations", "enabled") .put("simplebuilder.builderSuffix", "Factory") .put("simplebuilder.setterSuffix", "with") .build(); @@ -364,6 +365,7 @@ void readBuilderConfiguration_AllOptionsSet_ReadsCorrectly() { assertEquals(AccessModifier.PRIVATE, config.getBuilderConstructorAccess()); assertEquals(AccessModifier.PACKAGE_PRIVATE, config.getMethodAccess()); assertEquals(OptionState.DISABLED, config.generateVarArgsHelpers()); + assertEquals(OptionState.ENABLED, config.copyTypeAnnotations()); assertEquals("Factory", config.getBuilderSuffix()); assertEquals("with", config.getSetterSuffix()); } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 1c284424..6ddaf5fd 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -66,6 +66,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { .generateVarArgsHelpers(OptionState.ENABLED) .generateStringFormatHelpers(OptionState.ENABLED) .generateUnboxedOptional(OptionState.ENABLED) + .copyTypeAnnotations(OptionState.ENABLED) // Collection builder options .usingArrayListBuilder(OptionState.ENABLED) .usingArrayListBuilderWithElementBuilders(OptionState.ENABLED) @@ -95,6 +96,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { assertEquals(OptionState.ENABLED, config.generateVarArgsHelpers()); assertEquals(OptionState.ENABLED, config.generateStringFormatHelpers()); assertEquals(OptionState.ENABLED, config.generateUnboxedOptional()); + assertEquals(OptionState.ENABLED, config.copyTypeAnnotations()); assertEquals(OptionState.ENABLED, config.usingArrayListBuilder()); assertEquals(OptionState.ENABLED, config.usingArrayListBuilderWithElementBuilders()); assertEquals(OptionState.ENABLED, config.usingHashSetBuilder()); @@ -198,6 +200,7 @@ public Address() {} "-Asimplebuilder.generateVarArgsHelpers=false", "-Asimplebuilder.generateStringFormatHelpers=false", "-Asimplebuilder.generateUnboxedOptional=false", + "-Asimplebuilder.copyTypeAnnotations=false", "-Asimplebuilder.usingArrayListBuilder=false", "-Asimplebuilder.usingArrayListBuilderWithElementBuilders=false", "-Asimplebuilder.usingHashSetBuilder=false", From 6a95e83f470677637faa2c6e4d8b629b7e453276 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 23:11:33 +0100 Subject: [PATCH 6/9] Adding equal and hashcode functions to DTOs for finding/removing from list --- .../processor/dtos/AnnotationDto.java | 25 +++++++++++++++++++ .../builders/processor/dtos/TypeName.java | 25 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/AnnotationDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/AnnotationDto.java index b06cad0c..04d9f9e3 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/AnnotationDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/AnnotationDto.java @@ -26,6 +26,8 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; /** * DTO representing an annotation to be copied from the target class field to the builder class @@ -79,4 +81,27 @@ public Map getMembers() { public void addMember(String name, String value) { this.members.put(name, value); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + AnnotationDto that = (AnnotationDto) o; + + return new EqualsBuilder() + .append(annotationType, that.annotationType) + .append(members, that.members) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(annotationType).append(members).toHashCode(); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java index e4c7540a..c42f999d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java @@ -27,6 +27,8 @@ import static java.util.Objects.requireNonNull; import java.util.Optional; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; /** * TypeName is the unambiguously definition of a type. Holding name of class and package. Could be @@ -110,4 +112,27 @@ public Optional getInnerType() { public static TypeName of(Class clazz) { return new TypeName(clazz.getPackage().getName(), clazz.getSimpleName()); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + TypeName typeName = (TypeName) o; + + return new EqualsBuilder() + .append(packageName, typeName.packageName) + .append(className, typeName.className) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(packageName).append(className).toHashCode(); + } } From 138cdf810ef5816ab4bd950deccc3064e137d9bf Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 23:12:18 +0100 Subject: [PATCH 7/9] Adding test for possible duplicated annotations --- .../processor/TypeUseAnnotationTest.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java index 47a30e1d..ff052cf8 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java @@ -326,4 +326,92 @@ public Data(@ParamOnly String value, @TypeUse List list) { // Verify that the @TypeUse annotation IS present on the list field contains("TrackedValue<@TypeUse List> list")); } + + @Test + void typeUseAnnotations_whenDisabled_areNotCopied() { + String packageName = "test.typeuse.disabled"; + + JavaFileObject notNullAnnotation = + createMockAnnotation(packageName + ".annotations", "NotNull", "ElementType.TYPE_USE"); + + JavaFileObject person = + JavaFileObjects.forSourceString( + packageName + ".Person", + """ + package test.typeuse.disabled; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.enums.OptionState; + import test.typeuse.disabled.annotations.NotNull; + import java.util.List; + + @SimpleBuilder(options = @SimpleBuilder.Options(copyTypeAnnotations = OptionState.DISABLED)) + public class Person { + private final List<@NotNull String> nicknames; + + public Person(List<@NotNull String> nicknames) { + this.nicknames = nicknames; + } + + public List<@NotNull String> getNicknames() { return nicknames; } + } + """); + + Compilation compilation = compileSources(notNullAnnotation, person); + String generatedCode = loadGeneratedSource(compilation, "PersonBuilder"); + ProcessorAsserts.assertGenerationSucceeded(compilation, "PersonBuilder", generatedCode); + + // Verify that TYPE_USE annotations are NOT preserved in builder field + ProcessorAsserts.assertingResult( + generatedCode, + // Builder field should NOT have TYPE_USE annotation + notContains("TrackedValue> nicknames"), + contains("TrackedValue> nicknames"), + // Setter method parameter should NOT have TYPE_USE annotation + notContains("nicknames(List<@NotNull String> nicknames)"), + contains("nicknames(List nicknames)")); + } + + @Test + void hybridAnnotations_shouldBeDeduplicated() { + String packageName = "test.target.hybrid"; + + // Create an annotation that targets BOTH parameters AND types + JavaFileObject hybridAnnotation = + createMockAnnotation( + packageName + ".annotations", "Hybrid", "ElementType.PARAMETER, ElementType.TYPE_USE"); + + JavaFileObject data = + JavaFileObjects.forSourceString( + packageName + ".Data", + """ + package test.target.hybrid; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import test.target.hybrid.annotations.Hybrid; + import java.util.List; + + @SimpleBuilder + public class Data { + private final @Hybrid List items; + + public Data(@Hybrid List items) { + this.items = items; + } + + public List getItems() { return items; } + } + """); + + Compilation compilation = compileSources(hybridAnnotation, data); + String generatedCode = loadGeneratedSource(compilation, "DataBuilder"); + ProcessorAsserts.assertGenerationSucceeded(compilation, "DataBuilder", generatedCode); + + // Verify deduplication + ProcessorAsserts.assertingResult( + generatedCode, + // Should NOT contain duplicate annotations (e.g. "@Hybrid @Hybrid") + notContains("@Hybrid @Hybrid"), + // Should contain single annotation on the list + contains("TrackedValue<@Hybrid List> items"), + contains("items(@Hybrid List items)")); + } } From 4c63b2d25ec1396e4fb9bfe59eca19ec66a2885d Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 23:15:53 +0100 Subject: [PATCH 8/9] Adding configuration check for taking over type-annotations --- .../simple/builders/processor/util/JavaLangMapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java index 525e4ea6..dbad15a0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java @@ -460,7 +460,7 @@ protected TypeName defaultAction(TypeMirror e, Void p) { }, null); - if (typeName != null) { + if (typeName != null && context.getConfiguration().shouldCopyTypeAnnotations()) { List annotations = FieldAnnotationExtractor.extractAnnotations(typeOfParameter, context); annotations.forEach(typeName::addAnnotation); From 164b20f54c096428da8061499a4350098eca4f4b Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 23:17:39 +0100 Subject: [PATCH 9/9] Removing duplicated annotations on parameters --- .../builders/processor/util/BuilderDefinitionCreator.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index f4d0c1ce..a943e912 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -842,6 +842,10 @@ private static Optional createFieldDto( // Extract annotations from the field parameter List annotations = FieldAnnotationExtractor.extractAnnotations(param, context); + // Annotations could be assigned to Type or Parameter. + // To avoid duplication in generated code, we need to remove the duplications here. + annotations.removeAll(fieldType.getAnnotations()); + // Check if field has non-null constraint (annotation or primitive type) if (FieldAnnotationExtractor.hasNonNullConstraint(param) || fieldTypeMirror.getKind().isPrimitive()) {