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 46bbf140..abc4d56b 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 @@ -33,10 +33,21 @@ import org.javahelpers.simple.builders.core.enums.OptionState; /** - * Annotation to mark classes for builder generation. + * Annotation to mark classes and records for builder generation. * - *
Triggers generation of a fluent builder class with support for various patterns and helper - * methods. Can be used standalone or combined with {@link Options} for fine-grained control. + *
Place this annotation directly on a class or record to trigger generation of a fluent builder + * class with support for various patterns and helper methods. Can be used standalone or combined + * with {@link Options} for fine-grained control. + * + *
When to use {@code @SimpleBuilder} vs {@link Template}: + * + *
Available configuration options: * @@ -54,6 +65,14 @@ * *
Use {@link Template} to create reusable configuration presets. * + *
This annotation is {@link Inherited}: a subclass of an annotated type is treated as if it also + * carried {@code @SimpleBuilder} for the purpose of triggering builder generation, unless it is + * explicitly excluded via {@link Ignore4BuilderGeneration}. The {@link Template} meta-annotation is + * {@link Inherited} as well, so custom template annotations that are themselves {@code @Inherited} + * propagate to subclasses in the same way. Note that configuration options declared on the parent's + * {@code @SimpleBuilder(options = ...)} or template are not yet applied to inherited subclass + * builders; subclasses currently use default options (see issue #248). + * *
Related annotations: * *
This allows you to create custom annotations that pre-configure SimpleBuilder options. The - * custom annotation itself will be treated as @SimpleBuilder by the processor and will + *
This meta-annotation is placed on a custom annotation declaration (i.e., an + * {@code @interface}) to pre-configure SimpleBuilder options. The custom annotation can then be + * applied to classes and records just like {@link SimpleBuilder}, and the processor will * automatically apply the configured options. * + *
This annotation can only be placed on annotation types ({@link + * ElementType#ANNOTATION_TYPE}); it cannot be used directly on a class or record. Use {@link + * SimpleBuilder} for direct one-off annotation of classes, or use this meta-annotation to define + * a reusable custom annotation for a shared configuration across many classes. + * + *
This meta-annotation is {@link Inherited}. Note that this only controls inheritance of the + * {@code @SimpleBuilder.Template} meta-annotation itself; for a custom template annotation to + * propagate to unannotated subclasses, the custom annotation must additionally be declared with + * {@code @Inherited}. Without {@code @Inherited} on the custom annotation, only the exact type + * carrying it gets a builder. As with {@link SimpleBuilder}, configuration options declared on the + * template are not yet applied to inherited subclass builders; subclasses currently use default + * options (see issue #248). + * *
Example: * *
{@code
@@ -716,6 +750,23 @@
* }
* }
*
+ * To make the template propagate to subclasses, add {@code @Inherited} to the custom + * annotation: + * + *
{@code
+ * @SimpleBuilder.Template(options = @SimpleBuilder.Options(...))
+ * @Inherited
+ * @Retention(RetentionPolicy.CLASS)
+ * @Target(ElementType.TYPE)
+ * public @interface FullFeaturedBuilder {}
+ *
+ * @FullFeaturedBuilder
+ * public class ParentDto { ... }
+ *
+ * // ChildDto also gets a builder, because @FullFeaturedBuilder is @Inherited.
+ * public class ChildDto extends ParentDto { ... }
+ * }
+ *
* Related annotations: * *
Both {@code @SimpleBuilder} and the {@code @SimpleBuilder.Template} meta-annotation are + * meta-annotated with {@code @Inherited}, so unannotated subclasses of an annotated parent also get + * a builder. Custom template annotations that are themselves {@code @Inherited} propagate to + * subclasses in the same way. See issue #244. + */ +class BuilderAnnotationInheritanceTest { + + private Compilation compile(JavaFileObject... sourceFiles) { + return ProcessorTestUtils.createCompiler().compile(sourceFiles); + } + + /** + * An unannotated subclass of a class annotated with {@code @SimpleBuilder} must itself get a + * builder, because {@code @SimpleBuilder} is {@code @Inherited}. The parent's builder is still + * generated as well. + */ + @Test + void unannotatedSubclassGetsBuilderFromInheritedSimpleBuilder() { + JavaFileObject parentSource = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class ParentDto { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + JavaFileObject childSource = + ProcessorTestUtils.forSource( + """ + package test; + + public class ChildDto extends ParentDto { + private int age; + + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + } + """); + + Compilation compilation = compile(parentSource, childSource); + + assertThat(compilation).succeededWithoutWarnings(); + + String parentBuilder = loadGeneratedSource(compilation, "ParentDtoBuilder"); + assertGenerationSucceeded(compilation, "ParentDtoBuilder", parentBuilder); + assertContaining(parentBuilder, "public ParentDtoBuilder name(String name)"); + + String childBuilder = loadGeneratedSource(compilation, "ChildDtoBuilder"); + assertGenerationSucceeded(compilation, "ChildDtoBuilder", childBuilder); + // The child builder must expose setters for both the inherited field and its own field. + assertContaining(childBuilder, "public ChildDtoBuilder name(String name)"); + assertContaining(childBuilder, "public ChildDtoBuilder age(int age)"); + } + + /** + * An unannotated subclass of a class carrying a custom {@code @Inherited} template annotation + * (meta-annotated with {@code @SimpleBuilder.Template}) must itself get a builder. The parent's + * builder is still generated as well, and the template's options apply to both. + */ + @Test + void unannotatedSubclassGetsBuilderFromInheritedTemplate() { + JavaFileObject templateAnnotation = + ProcessorTestUtils.forSource( + """ + package test; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Inherited; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder.Template(options = @SimpleBuilder.Options()) + @Inherited + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + public @interface InheritedTemplate {} + """); + + JavaFileObject parentSource = + ProcessorTestUtils.forSource( + """ + package test; + + @InheritedTemplate + public class ParentDto { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + JavaFileObject childSource = + ProcessorTestUtils.forSource( + """ + package test; + + public class ChildDto extends ParentDto { + private int age; + + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + } + """); + + Compilation compilation = compile(templateAnnotation, parentSource, childSource); + + assertThat(compilation).succeededWithoutWarnings(); + + String parentBuilder = loadGeneratedSource(compilation, "ParentDtoBuilder"); + assertGenerationSucceeded(compilation, "ParentDtoBuilder", parentBuilder); + assertContaining(parentBuilder, "public ParentDtoBuilder name(String name)"); + + String childBuilder = loadGeneratedSource(compilation, "ChildDtoBuilder"); + assertGenerationSucceeded(compilation, "ChildDtoBuilder", childBuilder); + // The child builder must expose setters for both the inherited field and its own field. + assertContaining(childBuilder, "public ChildDtoBuilder name(String name)"); + assertContaining(childBuilder, "public ChildDtoBuilder age(int age)"); + } + + /** + * A subclass annotated with {@code @Ignore4BuilderGeneration} must NOT get a builder even though + * it would otherwise inherit {@code @SimpleBuilder} from its parent. The parent's builder is + * still generated. + */ + @Test + void subclassOptedOutViaIgnoreDoesNotGetInheritedBuilder() { + JavaFileObject parentSource = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class ParentDto { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + JavaFileObject childSource = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration; + + @Ignore4BuilderGeneration + public class ChildDto extends ParentDto { } + """); + + Compilation compilation = compile(parentSource, childSource); + + assertThat(compilation).succeededWithoutWarnings(); + + String parentBuilder = loadGeneratedSource(compilation, "ParentDtoBuilder"); + assertGenerationSucceeded(compilation, "ParentDtoBuilder", parentBuilder); + + assertNoBuilderGenerated( + compilation, + "ChildDto", + "ChildDto builder should not have been generated due to @Ignore4BuilderGeneration"); + } + + /** + * A grandchild of an annotated type must also inherit {@code @SimpleBuilder} across multiple + * levels of the type hierarchy. + */ + @Test + void grandchildInheritsSimpleBuilderAcrossMultipleLevels() { + JavaFileObject parentSource = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class GrandParentDto { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + JavaFileObject middleSource = + ProcessorTestUtils.forSource( + """ + package test; + + public class MiddleDto extends GrandParentDto { + private int age; + + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + } + """); + + JavaFileObject childSource = + ProcessorTestUtils.forSource( + """ + package test; + + public class ChildDto extends MiddleDto { + private boolean active; + + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } + } + """); + + Compilation compilation = compile(parentSource, middleSource, childSource); + + assertThat(compilation).succeededWithoutWarnings(); + + assertGenerationSucceeded( + compilation, + "GrandParentDtoBuilder", + loadGeneratedSource(compilation, "GrandParentDtoBuilder")); + assertGenerationSucceeded( + compilation, "MiddleDtoBuilder", loadGeneratedSource(compilation, "MiddleDtoBuilder")); + assertGenerationSucceeded( + compilation, "ChildDtoBuilder", loadGeneratedSource(compilation, "ChildDtoBuilder")); + } +} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/Ignore4BuilderGenerationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/Ignore4BuilderGenerationTest.java index 31011bb5..3eb6c05b 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/Ignore4BuilderGenerationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/Ignore4BuilderGenerationTest.java @@ -27,6 +27,7 @@ import static com.google.testing.compile.CompilationSubject.assertThat; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertContaining; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded; +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertNoBuilderGenerated; 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.loadGeneratedSource; @@ -35,7 +36,6 @@ import javax.tools.JavaFileObject; import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; /** Tests for the {@code @Ignore4BuilderGeneration} opt-out annotation. */ @@ -45,20 +45,6 @@ private Compilation compile(JavaFileObject... sourceFiles) { return ProcessorTestUtils.createCompiler().compile(sourceFiles); } - /** - * Asserts that no generated source file for the given class builder exists. - * - * @param compilation the compilation result - * @param className the simple class name for which no builder should have been generated - * @param message the failure message - */ - private void assertNoBuilderGenerated(Compilation compilation, String className, String message) { - Assertions.assertTrue( - compilation.generatedSourceFiles().stream() - .noneMatch(f -> f.getName().endsWith(className + "Builder.java")), - message); - } - /** * (a) A subclass that inherits a template annotation from its parent and is annotated with * {@code @Ignore4BuilderGeneration} must NOT get a builder, while the parent's builder is still diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java index c5d94a2e..ac6c4ecf 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java @@ -119,6 +119,24 @@ public static void assertHadNoteContaining(Compilation compilation, String... no } } + /** + * Asserts that no generated source file for the given class' builder exists in the compilation. + * + *
The check is based on the simple class name: a file whose name ends with {@code
+ *