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 3bd1170c..18fd3a1d 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
@@ -41,6 +41,28 @@
/** Extractor for field annotations, converting them from Java model elements to DTOs. */
public final class FieldAnnotationExtractor {
+ /**
+ * List of predicates that determine which annotations should be skipped (not copied to the
+ * builder). Each predicate receives the fully qualified annotation name and returns true if the
+ * annotation should be filtered out.
+ *
+ *
To add a new filter, simply add a new predicate to this list with a descriptive comment.
+ */
+ private static final java.util.List> ANNOTATION_FILTERS =
+ java.util.List.of(
+ // Skip SimpleBuilder framework annotations
+ name -> name.startsWith("org.javahelpers.simple.builders."),
+ // Skip code generation metadata annotations
+ name -> name.equals("javax.annotation.Generated"),
+ name -> name.equals("javax.annotation.processing.Generated"),
+ // Skip compiler-only annotations not relevant for builder parameters
+ name -> name.equals("java.lang.SuppressWarnings"),
+ // Skip @Valid annotation for cascading validation (jakarta.validation.Valid /
+ // javax.validation.Valid) - only meaningful on fields or method return types, not on
+ // builder method parameters where individual values are set
+ name -> name.equals("jakarta.validation.Valid"),
+ name -> name.equals("javax.validation.Valid"));
+
private FieldAnnotationExtractor() {
// Private constructor to prevent instantiation
}
@@ -192,17 +214,7 @@ private static String formatArray(List> list) {
* @return true if the annotation should be skipped, false otherwise
*/
private static boolean shouldSkipAnnotation(String qualifiedName) {
- // Skip SimpleBuilder framework annotations
- if (qualifiedName.startsWith("org.javahelpers.simple.builders.")) {
- return true;
- }
- // Skip generated code annotations (these are for code generation metadata, not validation)
- if (qualifiedName.equals("javax.annotation.Generated")
- || qualifiedName.equals("javax.annotation.processing.Generated")) {
- return true;
- }
- // Skip compiler-only annotations that are not relevant for builder parameters
- return qualifiedName.equals("java.lang.SuppressWarnings");
+ return ANNOTATION_FILTERS.stream().anyMatch(filter -> filter.test(qualifiedName));
}
/**
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/AnnotationCopyTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/AnnotationCopyTest.java
index b795d1bc..c78b800a 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/AnnotationCopyTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/AnnotationCopyTest.java
@@ -2,6 +2,7 @@
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;
@@ -25,37 +26,15 @@ void annotations_copiedToBuilderFields() {
String packageName = "test";
JavaFileObject notNullAnnotation =
- JavaFileObjects.forSourceString(
- packageName + ".annotations.NotNull",
- """
- package test.annotations;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.PARAMETER})
- public @interface NotNull {
- }
- """);
+ createMockAnnotation(
+ packageName + ".annotations", "NotNull", "ElementType.FIELD, ElementType.PARAMETER");
JavaFileObject customAnnotation =
- JavaFileObjects.forSourceString(
- packageName + ".annotations.CustomAnnotation",
- """
- package test.annotations;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.PARAMETER})
- public @interface CustomAnnotation {
- String value() default "";
- }
- """);
+ createMockAnnotation(
+ packageName + ".annotations",
+ "CustomAnnotation",
+ "ElementType.FIELD, ElementType.PARAMETER",
+ "String value() default \"\";");
JavaFileObject person =
JavaFileObjects.forSourceString(
@@ -101,36 +80,10 @@ void annotations_constructorParameters_copiedToBuilderFields() {
String packageName = "test.annotations.constructor";
JavaFileObject notNullAnnotation =
- JavaFileObjects.forSourceString(
- packageName + ".NotNull",
- """
- package test.annotations.constructor;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.PARAMETER})
- public @interface NotNull {
- }
- """);
+ createMockAnnotation(packageName, "NotNull", "ElementType.FIELD, ElementType.PARAMETER");
JavaFileObject positiveAnnotation =
- JavaFileObjects.forSourceString(
- packageName + ".Positive",
- """
- package test.annotations.constructor;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.PARAMETER})
- public @interface Positive {
- }
- """);
+ createMockAnnotation(packageName, "Positive", "ElementType.FIELD, ElementType.PARAMETER");
JavaFileObject product =
JavaFileObjects.forSourceString(
@@ -174,20 +127,7 @@ void annotations_frameworkAnnotations_notCopied() {
String packageName = "test.annotations.filtered";
JavaFileObject notNullAnnotation =
- JavaFileObjects.forSourceString(
- packageName + ".NotNull",
- """
- package test.annotations.filtered;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.PARAMETER})
- public @interface NotNull {
- }
- """);
+ createMockAnnotation(packageName, "NotNull", "ElementType.FIELD, ElementType.PARAMETER");
JavaFileObject person =
JavaFileObjects.forSourceString(
@@ -255,41 +195,32 @@ public enum Priority {
// Create a complex annotation with various value types
JavaFileObject complexAnnotation =
- JavaFileObjects.forSourceString(
- packageName + ".ComplexAnnotation",
+ createMockAnnotation(
+ packageName,
+ "ComplexAnnotation",
+ "ElementType.FIELD, ElementType.PARAMETER",
"""
- package test.complex;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
+ // Primitives
+ int intValue() default 42;
+ long longValue() default 100L;
+ boolean boolValue() default true;
+ double doubleValue() default 3.14;
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.PARAMETER})
- public @interface ComplexAnnotation {
- // Primitives
- int intValue() default 42;
- long longValue() default 100L;
- boolean boolValue() default true;
- double doubleValue() default 3.14;
+ // String
+ String stringValue() default "default";
- // String
- String stringValue() default "default";
+ // Enum
+ Priority priority() default Priority.MEDIUM;
- // Enum
- Priority priority() default Priority.MEDIUM;
+ // Class literal
+ Class> type() default String.class;
- // Class literal
- Class> type() default String.class;
+ // Array
+ String[] tags() default {};
+ int[] numbers() default {};
- // Array
- String[] tags() default {};
- int[] numbers() default {};
-
- // Nested annotation
- Metadata metadata() default @Metadata(author = "unknown", version = 1);
- }
- """);
+ // Nested annotation
+ Metadata metadata() default @Metadata(author = "unknown", version = 1);""");
JavaFileObject task =
JavaFileObjects.forSourceString(
@@ -410,21 +341,11 @@ void annotations_frameworkAnnotations_filtered() {
// Create an annotation in the SimpleBuilder framework package
JavaFileObject frameworkAnnotation =
- JavaFileObjects.forSourceString(
- "org.javahelpers.simple.builders.custom.FrameworkAnnotation",
- """
- package org.javahelpers.simple.builders.custom;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.PARAMETER})
- public @interface FrameworkAnnotation {
- String value() default "";
- }
- """);
+ createMockAnnotation(
+ "org.javahelpers.simple.builders.custom",
+ "FrameworkAnnotation",
+ "ElementType.FIELD, ElementType.PARAMETER",
+ "String value() default \"\";");
JavaFileObject entity =
JavaFileObjects.forSourceString(
@@ -467,21 +388,11 @@ void annotations_customAnnotations_copiedCorrectly() {
// Create a custom annotation that should NOT be filtered
JavaFileObject validAnnotation =
- JavaFileObjects.forSourceString(
- packageName + ".ValidAnnotation",
- """
- package test.custom;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.PARAMETER})
- public @interface ValidAnnotation {
- String value() default "";
- }
- """);
+ createMockAnnotation(
+ packageName,
+ "ValidAnnotation",
+ "ElementType.FIELD, ElementType.PARAMETER",
+ "String value() default \"\";");
JavaFileObject model =
JavaFileObjects.forSourceString(
@@ -520,21 +431,11 @@ void annotations_generatedAnnotations_notCopied() {
// Create Generated annotation (commonly used by code generators)
JavaFileObject generatedAnnotation =
- JavaFileObjects.forSourceString(
- "javax.annotation.Generated",
- """
- package javax.annotation;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Retention(RetentionPolicy.SOURCE)
- @Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
- public @interface Generated {
- String[] value();
- }
- """);
+ createMockAnnotation(
+ "javax.annotation",
+ "Generated",
+ "ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER",
+ "String[] value();");
JavaFileObject model =
JavaFileObjects.forSourceString(
@@ -575,22 +476,11 @@ void annotations_emptyArrayValues_formattedCorrectly() {
String packageName = "test.emptyarray";
JavaFileObject arrayAnnotation =
- JavaFileObjects.forSourceString(
- packageName + ".ArrayAnnotation",
- """
- package test.emptyarray;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.PARAMETER})
- public @interface ArrayAnnotation {
- String[] tags() default {};
- int[] numbers() default {};
- }
- """);
+ createMockAnnotation(
+ packageName,
+ "ArrayAnnotation",
+ "ElementType.FIELD, ElementType.PARAMETER",
+ "String[] tags() default {};\n int[] numbers() default {};");
JavaFileObject data =
JavaFileObjects.forSourceString(
@@ -627,22 +517,11 @@ void annotations_mixedPrimitiveArrays_formattedCorrectly() {
String packageName = "test.mixedarray";
JavaFileObject rangeAnnotation =
- JavaFileObjects.forSourceString(
- packageName + ".Range",
- """
- package test.mixedarray;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.FIELD, ElementType.PARAMETER})
- public @interface Range {
- int[] values();
- double[] decimals() default {1.0, 2.0};
- }
- """);
+ createMockAnnotation(
+ packageName,
+ "Range",
+ "ElementType.FIELD, ElementType.PARAMETER",
+ "int[] values();\n double[] decimals() default {1.0, 2.0};");
JavaFileObject measurement =
JavaFileObjects.forSourceString(
@@ -680,4 +559,285 @@ public MeasurementBuilder sensor(
public MeasurementBuilder sensor(
@Range(values = {0, 100, 255}, decimals = {0.5, 1.5, 2.5}) String format,"""));
}
+
+ @Test
+ void annotations_validAnnotation_jakartaValidation_filteredFromBuilderParameters() {
+ String packageName = "test.jakarta";
+
+ JavaFileObject address =
+ JavaFileObjects.forSourceString(
+ packageName + ".Address",
+ """
+ package test.jakarta;
+
+ public class Address {
+ private String street;
+ private String city;
+
+ public String getStreet() { return street; }
+ public void setStreet(String street) { this.street = street; }
+
+ public String getCity() { return city; }
+ public void setCity(String city) { this.city = city; }
+ }
+ """);
+
+ JavaFileObject person =
+ JavaFileObjects.forSourceString(
+ packageName + ".Person",
+ """
+ package test.jakarta;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ import jakarta.validation.Valid;
+ import jakarta.validation.constraints.NotNull;
+
+ @SimpleBuilder
+ public class Person {
+ private String name;
+ private Address address;
+
+ public String getName() { return name; }
+ public void setName(@NotNull String name) { this.name = name; }
+
+ public Address getAddress() { return address; }
+ public void setAddress(@Valid @NotNull Address address) { this.address = address; }
+ }
+ """);
+
+ Compilation compilation =
+ compileSources(
+ createJakartaValidAnnotation(), createJakartaNotNullAnnotation(), address, person);
+ String generatedCode = loadGeneratedSource(compilation, "PersonBuilder");
+ ProcessorAsserts.assertGenerationSucceeded(compilation, "PersonBuilder", generatedCode);
+
+ ProcessorAsserts.assertingResult(
+ generatedCode,
+ contains("name(@NotNull String name)"),
+ contains("address(@NotNull Address address)"));
+
+ ProcessorAsserts.assertNotContaining(generatedCode, "@Valid");
+ }
+
+ @Test
+ void annotations_validAnnotation_javaxValidation_filteredFromBuilderParameters() {
+ String packageName = "test.javax";
+
+ JavaFileObject contact =
+ JavaFileObjects.forSourceString(
+ packageName + ".Contact",
+ """
+ package test.javax;
+
+ public class Contact {
+ private String email;
+ private String phone;
+
+ public String getEmail() { return email; }
+ public void setEmail(String email) { this.email = email; }
+
+ public String getPhone() { return phone; }
+ public void setPhone(String phone) { this.phone = phone; }
+ }
+ """);
+
+ JavaFileObject customer =
+ JavaFileObjects.forSourceString(
+ packageName + ".Customer",
+ """
+ package test.javax;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ import javax.validation.Valid;
+ import javax.validation.constraints.Size;
+
+ @SimpleBuilder
+ public class Customer {
+ private String id;
+ private Contact contact;
+
+ public String getId() { return id; }
+ public void setId(@Size(min = 5, max = 20) String id) { this.id = id; }
+
+ public Contact getContact() { return contact; }
+ public void setContact(@Valid Contact contact) { this.contact = contact; }
+ }
+ """);
+
+ Compilation compilation =
+ compileSources(
+ createMockAnnotation(
+ "javax.validation",
+ "Valid",
+ "ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD"),
+ createMockAnnotation(
+ "javax.validation.constraints",
+ "Size",
+ "ElementType.FIELD, ElementType.PARAMETER",
+ "int min() default 0;\n int max() default Integer.MAX_VALUE;"),
+ contact,
+ customer);
+ String generatedCode = loadGeneratedSource(compilation, "CustomerBuilder");
+ ProcessorAsserts.assertGenerationSucceeded(compilation, "CustomerBuilder", generatedCode);
+
+ ProcessorAsserts.assertingResult(
+ generatedCode,
+ contains("id(@Size(min = 5, max = 20) String id)"),
+ contains("contact(Contact contact)"));
+
+ ProcessorAsserts.assertNotContaining(generatedCode, "@Valid");
+ }
+
+ @Test
+ void annotations_validAnnotation_constructorParameters_filteredFromBuilderParameters() {
+ String packageName = "test.constructor";
+
+ JavaFileObject department =
+ JavaFileObjects.forSourceString(
+ packageName + ".Department",
+ """
+ package test.constructor;
+
+ public class Department {
+ private String name;
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ }
+ """);
+
+ JavaFileObject employee =
+ JavaFileObjects.forSourceString(
+ packageName + ".Employee",
+ """
+ package test.constructor;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ import jakarta.validation.Valid;
+ import jakarta.validation.constraints.NotNull;
+
+ @SimpleBuilder
+ public class Employee {
+ private final String name;
+ private final Department department;
+
+ public Employee(
+ @NotNull String name,
+ @Valid @NotNull Department department) {
+ this.name = name;
+ this.department = department;
+ }
+
+ public String getName() { return name; }
+ public Department getDepartment() { return department; }
+ }
+ """);
+
+ Compilation compilation =
+ compileSources(
+ createJakartaValidAnnotation(), createJakartaNotNullAnnotation(), department, employee);
+ String generatedCode = loadGeneratedSource(compilation, "EmployeeBuilder");
+ ProcessorAsserts.assertGenerationSucceeded(compilation, "EmployeeBuilder", generatedCode);
+
+ ProcessorAsserts.assertingResult(
+ generatedCode,
+ contains("name(@NotNull String name)"),
+ contains("department(@NotNull Department department)"));
+
+ ProcessorAsserts.assertNotContaining(generatedCode, "@Valid");
+ }
+
+ @Test
+ void annotations_validAnnotation_mixedWithConstraints_onlyValidFiltered() {
+ String packageName = "test.mixed";
+
+ JavaFileObject metadata =
+ JavaFileObjects.forSourceString(
+ packageName + ".Metadata",
+ """
+ package test.mixed;
+
+ public class Metadata {
+ private String key;
+ private String value;
+
+ public String getKey() { return key; }
+ public void setKey(String key) { this.key = key; }
+
+ public String getValue() { return value; }
+ public void setValue(String value) { this.value = value; }
+ }
+ """);
+
+ JavaFileObject document =
+ JavaFileObjects.forSourceString(
+ packageName + ".Document",
+ """
+ package test.mixed;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ import jakarta.validation.Valid;
+ import jakarta.validation.constraints.NotNull;
+ import jakarta.validation.constraints.Size;
+
+ @SimpleBuilder
+ public class Document {
+ private String title;
+ private Metadata metadata;
+
+ public String getTitle() { return title; }
+ public void setTitle(@NotNull @Size(min = 1, max = 100) String title) {
+ this.title = title;
+ }
+
+ public Metadata getMetadata() { return metadata; }
+ public void setMetadata(@Valid @NotNull Metadata metadata) {
+ this.metadata = metadata;
+ }
+ }
+ """);
+
+ Compilation compilation =
+ compileSources(
+ createJakartaValidAnnotation(),
+ createJakartaNotNullAnnotation(),
+ createMockAnnotation(
+ "jakarta.validation.constraints",
+ "Size",
+ "ElementType.FIELD, ElementType.PARAMETER",
+ "int min() default 0;\n int max() default Integer.MAX_VALUE;"),
+ metadata,
+ document);
+ String generatedCode = loadGeneratedSource(compilation, "DocumentBuilder");
+ ProcessorAsserts.assertGenerationSucceeded(compilation, "DocumentBuilder", generatedCode);
+
+ ProcessorAsserts.assertingResult(
+ generatedCode,
+ contains("title(@NotNull @Size(min = 1, max = 100) String title)"),
+ contains("metadata(@NotNull Metadata metadata)"));
+
+ ProcessorAsserts.assertNotContaining(generatedCode, "@Valid");
+ }
+
+ /**
+ * Creates a mock jakarta.validation.Valid annotation for testing.
+ *
+ * This helper is used multiple times across different test methods.
+ *
+ * @return a JavaFileObject representing the mocked Valid annotation
+ */
+ private JavaFileObject createJakartaValidAnnotation() {
+ return createMockAnnotation(
+ "jakarta.validation",
+ "Valid",
+ "ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD");
+ }
+
+ /**
+ * Creates a mock jakarta.validation.constraints.NotNull annotation for testing.
+ *
+ *
This helper is used multiple times across different test methods.
+ *
+ * @return a JavaFileObject representing the mocked NotNull annotation
+ */
+ private JavaFileObject createJakartaNotNullAnnotation() {
+ return createMockAnnotation(
+ "jakarta.validation.constraints", "NotNull", "ElementType.FIELD, ElementType.PARAMETER");
+ }
}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorTestUtils.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorTestUtils.java
index ab10e6e0..d7a70427 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorTestUtils.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorTestUtils.java
@@ -242,4 +242,71 @@ private static String[] buildSourceLines(
System.arraycopy(footer, 0, lines, header.length + body.length, footer.length);
return lines;
}
+
+ /**
+ * Creates a mock annotation for testing without any members.
+ *
+ *
Mock annotations are used instead of real dependencies because:
+ *
+ *
+ * - Annotation processors work at compile-time and only see annotation metadata, not
+ * implementations
+ *
- Avoids adding unnecessary runtime dependencies to the test classpath
+ *
- Ensures test isolation from external library versions
+ *
- Follows standard practice for annotation processor testing with Google Compile Testing
+ *
+ *
+ * The processor only cares about the qualified name, not the actual implementation.
+ *
+ * @param packageName the package name of the annotation (e.g., "jakarta.validation")
+ * @param annotationName the simple name of the annotation (e.g., "Valid")
+ * @param targets the annotation targets (e.g., "ElementType.FIELD, ElementType.PARAMETER")
+ * @return a JavaFileObject representing the mocked annotation
+ */
+ public static JavaFileObject createMockAnnotation(
+ String packageName, String annotationName, String targets) {
+ return createMockAnnotation(packageName, annotationName, targets, "");
+ }
+
+ /**
+ * Creates a mock annotation for testing with optional members.
+ *
+ *
Mock annotations are used instead of real dependencies because:
+ *
+ *
+ * - Annotation processors work at compile-time and only see annotation metadata, not
+ * implementations
+ *
- Avoids adding unnecessary runtime dependencies to the test classpath
+ *
- Ensures test isolation from external library versions
+ *
- Follows standard practice for annotation processor testing with Google Compile Testing
+ *
+ *
+ * The processor only cares about the qualified name, not the actual implementation.
+ *
+ * @param packageName the package name of the annotation (e.g., "jakarta.validation")
+ * @param annotationName the simple name of the annotation (e.g., "Valid")
+ * @param targets the annotation targets (e.g., "ElementType.FIELD, ElementType.PARAMETER")
+ * @param members the annotation members/attributes (e.g., "int min() default 0;")
+ * @return a JavaFileObject representing the mocked annotation
+ */
+ public static JavaFileObject createMockAnnotation(
+ String packageName, String annotationName, String targets, String members) {
+ String qualifiedName = packageName + "." + annotationName;
+ String membersSection = members.isEmpty() ? "" : "\n " + members + "\n";
+
+ return JavaFileObjects.forSourceString(
+ qualifiedName,
+ """
+ package %s;
+ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.RetentionPolicy;
+ import java.lang.annotation.Target;
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({%s})
+ public @interface %s {%s}
+ """
+ .formatted(packageName, targets, annotationName, membersSection));
+ }
}