Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
fff0ff1
Make @SimpleBuilder @Inherited to match documentation (#244)
AndreasIgel Aug 15, 2026
13a8fc8
Rename to BuilderAnnotationInheritanceTest and cover Template inherit…
AndreasIgel Aug 15, 2026
105310a
Refactoring code to move assertNoBuilderGenerated to common asserts
AndreasIgel Aug 15, 2026
2c83b78
fixing codeformat
AndreasIgel Aug 15, 2026
aca9ef6
Document @Inherited behavior of @SimpleBuilder.Template
AndreasIgel Aug 15, 2026
bd3bf93
Document options-inheritance limitation for inherited subclass builders
AndreasIgel Aug 15, 2026
e8e0352
Fix issue reference: #245 -> #248
AndreasIgel Aug 15, 2026
9776343
Clarify @SimpleBuilder vs @SimpleBuilder.Template usage in docs
AndreasIgel Aug 15, 2026
4b7723a
Link issue #248 from @SimpleBuilder.Template inheritance Javadoc
AndreasIgel Aug 15, 2026
218ea79
Remove redundant troubleshooting item about @SimpleBuilder.Template t…
AndreasIgel Aug 15, 2026
dbf2534
fix: inherit @SimpleBuilder options from parent classes (#248)
devin-ai-integration[bot] Aug 15, 2026
2f6595c
fix: scope-aware inheritance so direct annotations override inherited…
devin-ai-integration[bot] Aug 15, 2026
d16bf5c
docs: update Javadoc and apply formatting after scope-aware inheritan…
devin-ai-integration[bot] Aug 15, 2026
fdc4db0
build: add maven-surefire-plugin 3.5.6 to core/example so JUnit 5 tes…
devin-ai-integration[bot] Aug 15, 2026
ae8e4fc
Merge branch 'main' into devin/fix-issue-248-inherited-options
devin-ai-integration[bot] Aug 16, 2026
9f18720
build: revert JUnit/Surefire pom workarounds back to origin/main state
devin-ai-integration[bot] Aug 16, 2026
0b850ba
docs: clarify Javadoc for custom template annotation precedence
devin-ai-integration[bot] Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@
* 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).
* propagate to subclasses in the same way. Configuration options declared on the parent's
* {@code @SimpleBuilder(options = ...)} or template are also inherited by subclass builders.
*
* <p>Related annotations:
*
Expand Down Expand Up @@ -727,9 +726,9 @@
* {@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).
* carrying it gets a builder. As with {@link SimpleBuilder}, configuration options declared on
* the template are also applied to inherited subclass builders, as long as the custom template
* annotation is itself {@code @Inherited} and no direct annotation overrides it.
*
* <p>Example:
*
Expand Down
4 changes: 2 additions & 2 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public class ParentDto { ... }
public class ChildDto extends ParentDto { ... }
```

> **Note:** Inherited subclasses currently get a builder with **default options**, not the options declared on the parent's `@SimpleBuilder(options = ...)` or template. Options inheritance is tracked separately (see #248).
> **Note:** Inherited subclasses get a builder that also uses the options declared on the parent's `@SimpleBuilder(options = ...)` or template. If the subclass declares its own `@SimpleBuilder` or template annotation, those options take precedence.

## Excluding Types from Builder Generation

Expand Down Expand Up @@ -1158,7 +1158,7 @@ Or in compiler options:
3. **Retention and Target**: Add `@Retention(RetentionPolicy.CLASS)` and `@Target(ElementType.TYPE)`
4. **Don't combine**: Don't use `@SimpleBuilder` when using a template annotation
5. **Subclasses not getting a builder**: Add `@Inherited` to the custom template annotation so it propagates to unannotated subclasses (see [Template Annotations](#template-annotations) above). Without `@Inherited`, only the exact type carrying the annotation gets a builder.
6. **Subclass builder has wrong options**: Inherited subclass builders currently use default options, not the parent's `@SimpleBuilder.Options` or template options. This is a known limitation (see #248).
6. **Subclass builder has wrong options**: Ensure the custom template annotation is `@Inherited` and that the parent annotation declares the desired `@SimpleBuilder.Options`. Inherited options are applied to subclass builders; a subclass's own annotation overrides the inherited options.

### Builder Not Generated - Access Modifier Errors

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@

package org.javahelpers.simple.builders.processor.processing;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
Expand All @@ -39,19 +43,23 @@
/**
* Reads builder configuration from annotated elements.
*
* <p>This class analyzes {@link SimpleBuilder.Options} and {@link SimpleBuilder.Template}
* annotations on an element and extracts the raw configuration values without merging.
* <p>This class analyzes {@link SimpleBuilder.Options} and custom template annotations (annotations
* meta-annotated with {@link SimpleBuilder.Template}) on an element.
*
* <p>Priority order:
* <p>Priority order (highest to lowest):
*
* <ol>
* <li>{@code @SimpleBuilder(options = ...)} inline options (highest priority)
* <li>Custom template annotations (e.g., {@code @CustomBuilder})
* <li>Directly declared {@code @SimpleBuilder(options = ...)} inline options
* <li>Custom template annotations directly declared on the element
* <li>Inherited {@code @SimpleBuilder(options = ...)} inline options
* <li>Inherited custom template annotations
* <li>Global compiler arguments
* <li>Built-in defaults (lowest priority)
* <li>Built-in defaults
* </ol>
*
* <p>Note: If {@code @SimpleBuilder} is present, custom template annotations are ignored.
* <p>Custom template annotations are annotations that are themselves meta-annotated with
* {@code @SimpleBuilder.Template}; they are not placed directly on the class. Within each
* inheritance scope, {@code @SimpleBuilder} options take precedence over template options.
*/
public class BuilderConfigurationReader {
private static final String SIMPLE_BUILDER_ANNOTATION =
Expand Down Expand Up @@ -91,19 +99,30 @@ public BuilderConfiguration getGlobalConfiguration() {
/**
* Reads builder configuration from {@code @SimpleBuilder(options = ...)} inline options.
*
* <p>Directly declared options take precedence over inherited options from superclasses.
*
* <p>Returns null if the element has no {@code @SimpleBuilder} annotation.
*
* @param element the annotated element to analyze
* @return configuration from the inline options, or null if not present
*/
public BuilderConfiguration readFromInlineOptions(Element element) {
BuilderConfiguration direct = readFromInlineOptions(element, AnnotationScope.DIRECT);
if (direct != null) {
return direct;
}
return readFromInlineOptions(element, AnnotationScope.INHERITED);
}

private BuilderConfiguration readFromInlineOptions(Element element, AnnotationScope scope) {
AnnotationMirror simpleBuilderMirror =
extractAnnotationMirror(element, SIMPLE_BUILDER_ANNOTATION);
extractAnnotationMirror(element, SIMPLE_BUILDER_ANNOTATION, scope);
return extractOptionsFromAnnotationMirror(simpleBuilderMirror);
}

private AnnotationMirror extractAnnotationMirror(Element element, String annotationName) {
for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
private AnnotationMirror extractAnnotationMirror(
Element element, String annotationName, AnnotationScope scope) {
for (AnnotationMirror mirror : getAnnotationMirrors(element, scope)) {
if (mirror.getAnnotationType().toString().equals(annotationName)) {
return mirror;
}
Expand Down Expand Up @@ -227,25 +246,35 @@ private String extractEnumName(Object value) {
/**
* Reads builder configuration from a custom template annotation on the element.
*
* <p>Only checks for custom template annotations if {@code @SimpleBuilder} is NOT present. Looks
* for any custom annotation on the element that is itself annotated with
* {@code @SimpleBuilder.Template}.
* <p>Directly declared template annotations take precedence over inherited template annotations.
* If a {@code @SimpleBuilder} annotation is present in the same scope, template annotations in
* that scope are ignored.
*
* <p>Returns null if no template annotation is found or if {@code @SimpleBuilder} is present.
*
* @param element the annotated element to analyze
* @return configuration from the template annotation, or null if not present
*/
public BuilderConfiguration readFromTemplate(Element element) {
// If @SimpleBuilder is present, ignore template annotations
if (hasSimpleBuilderAnnotation(element)) {
BuilderConfiguration direct = readFromTemplate(element, AnnotationScope.DIRECT);
if (direct != null) {
return direct;
}
return readFromTemplate(element, AnnotationScope.INHERITED);
}

private BuilderConfiguration readFromTemplate(Element element, AnnotationScope scope) {
List<? extends AnnotationMirror> mirrors = getAnnotationMirrors(element, scope);

// If @SimpleBuilder is present in this scope, ignore template annotations in the same scope
if (containsSimpleBuilder(mirrors)) {
logger.debug(
"Template annotations ignored because @SimpleBuilder present", element.getSimpleName());
"Template annotations ignored because @SimpleBuilder is present in %s scope", scope);
return null;
}

// Check all annotations on the element to find one annotated with @SimpleBuilder.Template
for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
// Check all annotations in this scope to find one annotated with @SimpleBuilder.Template
for (AnnotationMirror mirror : mirrors) {
BuilderConfiguration templateConfig = checkForTemplateAnnotation(mirror, element);
if (templateConfig != null) {
logger.debug("Annotation based Configuration: %s", templateConfig.toString());
Expand All @@ -256,14 +285,33 @@ public BuilderConfiguration readFromTemplate(Element element) {
return null;
}

/**
* Checks if the element has a direct @SimpleBuilder annotation.
*
* @param element the element to check
* @return true if @SimpleBuilder is present
*/
private boolean hasSimpleBuilderAnnotation(Element element) {
private enum AnnotationScope {
DIRECT,
INHERITED
}

private List<? extends AnnotationMirror> getAnnotationMirrors(
Element element, AnnotationScope scope) {
if (scope == AnnotationScope.DIRECT) {
return element.getAnnotationMirrors();
}

Set<String> directAnnotationTypes = new HashSet<>();
for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
directAnnotationTypes.add(mirror.getAnnotationType().toString());
}

List<AnnotationMirror> inheritedMirrors = new ArrayList<>();
for (AnnotationMirror mirror : elementUtils.getAllAnnotationMirrors(element)) {
if (!directAnnotationTypes.contains(mirror.getAnnotationType().toString())) {
inheritedMirrors.add(mirror);
}
}
return inheritedMirrors;
}

private boolean containsSimpleBuilder(List<? extends AnnotationMirror> mirrors) {
for (AnnotationMirror mirror : mirrors) {
if (isSimpleBuilderAnnotation(mirror)) {
return true;
}
Expand Down Expand Up @@ -345,15 +393,19 @@ private BuilderConfiguration extractOptionsFromTemplateMirror(AnnotationMirror t
* <p>Priority chain (highest to lowest):
*
* <ol>
* <li>{@code @SimpleBuilder(options = ...)} inline options (highest priority)
* <li>Custom template annotations (only if {@code @SimpleBuilder} not present)
* <li>Directly declared {@code @SimpleBuilder(options = ...)} inline options
* <li>Custom template annotations directly declared on the element
* <li>Inherited {@code @SimpleBuilder(options = ...)} inline options
* <li>Inherited custom template annotations
* <li>Global compiler arguments
* <li>Built-in defaults
* </ol>
*
* <p>Note: If {@code @SimpleBuilder} is present, custom template annotations are completely
* ignored. The merge chain ensures that for each field: inline options override compiler args,
* which override defaults.
* <p>Custom template annotations are annotations that are themselves meta-annotated with
* {@code @SimpleBuilder.Template} and are placed directly on the class; the
* {@code @SimpleBuilder.Template} meta-annotation is not placed on the class itself. Within each
* inheritance scope, {@code @SimpleBuilder} options take precedence over template options. Direct
* annotations always override inherited annotations.
*
* @param element the annotated element to resolve configuration for
* @return the fully resolved configuration with all sources merged
Expand All @@ -362,14 +414,21 @@ public BuilderConfiguration resolveConfiguration(Element element) throws Builder
String elementName = element.getSimpleName().toString();
logger.debugStartOperation("Resolving configuration for element: %s", elementName);

BuilderConfiguration templateConfig = readFromTemplate(element);
BuilderConfiguration inlineConfig = readFromInlineOptions(element);
BuilderConfiguration inheritedTemplateConfig =
readFromTemplate(element, AnnotationScope.INHERITED);
BuilderConfiguration inheritedInlineConfig =
readFromInlineOptions(element, AnnotationScope.INHERITED);
BuilderConfiguration directTemplateConfig = readFromTemplate(element, AnnotationScope.DIRECT);
BuilderConfiguration directInlineConfig =
readFromInlineOptions(element, AnnotationScope.DIRECT);

BuilderConfiguration result =
BuilderConfiguration.DEFAULT
.merge(globalConfiguration)
.merge(templateConfig)
.merge(inlineConfig);
.merge(inheritedTemplateConfig)
.merge(inheritedInlineConfig)
.merge(directTemplateConfig)
.merge(directInlineConfig);

// Validate access modifiers and warn about problematic configurations
validateAccessModifiers(element, result);
Expand Down
Loading