From 7bf1e5214ec82eee54a3bf679618597a316119c2 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 10:42:07 +0000
Subject: [PATCH 001/170] processing: generalize ProcessingTarget to a typed
per-element carrier
---
.../processing/ProcessingTarget.java | 26 +++++++++----------
.../builders/processor/BuilderProcessor.java | 4 +--
.../processing/BuilderProcessingContext.java | 9 ++++---
3 files changed, 19 insertions(+), 20 deletions(-)
rename processor/src/main/java/org/javahelpers/{simple/builders/processor => codegen}/processing/ProcessingTarget.java (51%)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTarget.java
similarity index 51%
rename from processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java
rename to processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTarget.java
index f33d0478..4bd11b5e 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTarget.java
@@ -22,22 +22,20 @@
* SOFTWARE.
*/
-package org.javahelpers.simple.builders.processor.processing;
-
-import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
+package org.javahelpers.codegen.processing;
/**
- * Per-target processing state for the type whose builder is currently being generated.
+ * Per-target processing state for the type that is currently being generated.
*
- *
{@link org.javahelpers.simple.builders.processor.BuilderProcessor} sets one instance per
- * processed type on the {@link BuilderProcessingContext} before extraction starts, so downstream
- * analysis can reach it without threading both values through every call.
+ *
The processing driver sets one instance per processed type on the processing context before
+ * extraction starts, so downstream analysis can reach it without threading both values through
+ * every call.
*
- * @param configuration the resolved builder configuration for the current target
- * @param builderPackage the package the generated builder is written to. This is the configured
- * {@code packageName} option when set; otherwise the processed type's own package for
- * {@code @SimpleBuilder} targets, resp. the holder's package for {@code @SimpleBuilderFor}
- * targets. It cannot always be derived from the processed element itself, which is why it is
- * carried explicitly here
+ * @param configuration the resolved domain configuration for the current target
+ * @param targetPackage the package the generated class is written to. This is the configured output
+ * package when set; otherwise a package derived from the processed element itself. It cannot
+ * always be derived from the processed element itself, which is why it is carried explicitly
+ * here
+ * @param the domain configuration type
*/
-public record ProcessingTarget(BuilderConfiguration configuration, String builderPackage) {}
+public record ProcessingTarget(T configuration, String targetPackage) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index f666dad4..eb2a5b6d 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -56,6 +56,7 @@
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
+import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;
@@ -81,7 +82,6 @@
import org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext;
import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsEnum;
import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader;
-import org.javahelpers.simple.builders.processor.processing.ProcessingTarget;
import org.javahelpers.simple.builders.processor.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
@@ -644,7 +644,7 @@ private void assertTargetClassDoesNotExist(GenerationTargetClassDto classDef)
private void process(Element annotatedElement, BuilderConfiguration config, String builderPackage)
throws BuilderException {
- context.initProcessingTarget(new ProcessingTarget(config, builderPackage));
+ context.initProcessingTarget(new ProcessingTarget<>(config, builderPackage));
PerformanceTracker tracker = context.getPerformanceTracker();
// Track Builder Definition Extraction
tracker.startPhase();
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java
index 9bd05e69..d325683d 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java
@@ -28,6 +28,7 @@
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
+import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver;
import org.javahelpers.simple.builders.processor.generators.registry.GeneratorRegistry;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
@@ -46,7 +47,7 @@ public final class BuilderProcessingContext extends AnnotationProcessingContext
private final BuilderScopeResolver builderScopeResolver;
private GeneratorRegistry generatorRegistry;
- private ProcessingTarget processingTarget;
+ private ProcessingTarget processingTarget;
/**
* Creates a new processing context.
@@ -85,9 +86,9 @@ private static AnnotationProcessingOptions createOptions(
*
* @param processingTarget the resolved configuration and builder package of the current target
*/
- public void initProcessingTarget(ProcessingTarget processingTarget) {
+ public void initProcessingTarget(ProcessingTarget processingTarget) {
this.processingTarget = processingTarget;
- setGeneratedPackageName(processingTarget.builderPackage());
+ setGeneratedPackageName(processingTarget.targetPackage());
}
/**
@@ -105,7 +106,7 @@ public BuilderConfiguration getConfiguration() {
* @return the qualified package name of the generated builder
*/
public String getBuilderPackageName() {
- return this.processingTarget.builderPackage();
+ return this.processingTarget.targetPackage();
}
/**
From 96fb49548973f82fba53941edcc8b3471c20e4d7 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 10:46:41 +0000
Subject: [PATCH 002/170] =?UTF-8?q?processing:=20extract=20AnnotationEleme?=
=?UTF-8?q?ntSpec=20=E2=80=94=20declarative=20element=20selection,=20colle?=
=?UTF-8?q?ction=20only=20finds?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AnnotationElementSpec.java | 136 ++++++++++++++++++
.../codegen/processing/ElementCollection.java | 61 ++++++++
.../codegen/processing/ElementSelector.java | 47 ++++++
.../builders/processor/BuilderProcessor.java | 110 +++++---------
4 files changed, 276 insertions(+), 78 deletions(-)
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/ElementCollection.java
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
new file mode 100644
index 00000000..20f513dd
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
@@ -0,0 +1,136 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.TypeElement;
+import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser;
+import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
+
+/**
+ * Declarative specification of which elements a processor works on.
+ *
+ * The spec only finds elements: matching (direct or via meta-annotation), opt-out
+ * filtering and deterministic ordering. Reading annotation values is the domain's
+ * responsibility in the planning step — per-instance configuration, repeatable-annotation expansion
+ * and conflict detection are domain semantics the spec does not know.
+ */
+public final class AnnotationElementSpec {
+
+ private final List selectors;
+ private final String collectionPhase;
+
+ /**
+ * Creates a spec.
+ *
+ * @param selectors the named selection groups, in declaration order
+ * @param collectionPhase performance-tracking phase name the collection is reported under, or
+ * {@code null} to skip timing
+ */
+ public AnnotationElementSpec(List selectors, String collectionPhase) {
+ this.selectors = selectors;
+ this.collectionPhase = collectionPhase;
+ }
+
+ /**
+ * The performance-tracking phase the collection is reported under.
+ *
+ * @return the phase name, or {@code null} when untimed
+ */
+ public String collectionPhase() {
+ return collectionPhase;
+ }
+
+ /**
+ * Collects the elements selected by every selector in the given round.
+ *
+ * @param annotations the annotation types the round carries
+ * @param roundEnv the current round
+ * @param logger logger for opt-out diagnostics
+ * @return the per-selector element groups, sorted by simple name
+ */
+ public ElementCollection collect(
+ Set extends TypeElement> annotations, RoundEnvironment roundEnv, ProcessingLogger logger) {
+ Map> groups = new LinkedHashMap<>();
+ for (ElementSelector selector : selectors) {
+ Set selected = new HashSet<>();
+ // Direct matches go through the Class overload, which also unwraps repeatable containers
+ for (Class extends java.lang.annotation.Annotation> type : selector.annotationTypes()) {
+ selected.addAll(roundEnv.getElementsAnnotatedWith(type));
+ }
+ for (TypeElement annotation : matchingAnnotations(annotations, selector)) {
+ selected.addAll(roundEnv.getElementsAnnotatedWith(annotation));
+ }
+ if (selector.optOutAnnotation() != null) {
+ selected.removeIf(
+ element -> {
+ if (JavaLangAnalyser.findAnnotation(element, selector.optOutAnnotation())
+ .isPresent()) {
+ logger.debug(
+ "Skipping element '%s' due to @%s opt-out.",
+ element.getSimpleName(), selector.optOutAnnotation().getSimpleName());
+ return true;
+ }
+ return false;
+ });
+ }
+ groups.put(
+ selector.name(),
+ selected.stream()
+ .sorted(Comparator.comparing(element -> element.getSimpleName().toString()))
+ .toList());
+ }
+ return new ElementCollection(groups);
+ }
+
+ private static List matchingAnnotations(
+ Set extends TypeElement> annotations, ElementSelector selector) {
+ List result = new ArrayList<>();
+ for (TypeElement annotation : annotations) {
+ if (annotation.getKind() != ElementKind.ANNOTATION_TYPE) {
+ continue;
+ }
+ boolean direct =
+ selector.annotationTypes().stream()
+ .anyMatch(
+ type -> type.getCanonicalName().equals(annotation.getQualifiedName().toString()));
+ boolean meta =
+ selector.metaMarker() != null && annotation.getAnnotation(selector.metaMarker()) != null;
+ if (direct || meta) {
+ result.add(annotation);
+ }
+ }
+ return result;
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollection.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollection.java
new file mode 100644
index 00000000..04382ccc
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollection.java
@@ -0,0 +1,61 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import java.util.List;
+import java.util.Map;
+import javax.lang.model.element.Element;
+
+/**
+ * The result of {@link AnnotationElementSpec#collect}: per-selector-name, deterministically ordered
+ * element lists. Elements are sorted by simple name so generated output is reproducible.
+ */
+public final class ElementCollection {
+
+ private final Map> groups;
+
+ ElementCollection(Map> groups) {
+ this.groups = groups;
+ }
+
+ /**
+ * The elements selected by the named selector, sorted by simple name.
+ *
+ * @param selectorName name of an {@link ElementSelector}
+ * @return the selected elements, or an empty list when the selector matched nothing
+ */
+ public List byName(String selectorName) {
+ return groups.getOrDefault(selectorName, List.of());
+ }
+
+ /**
+ * Whether every selected group is empty.
+ *
+ * @return {@code true} when no selector matched any element
+ */
+ public boolean isEmpty() {
+ return groups.values().stream().allMatch(List::isEmpty);
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
new file mode 100644
index 00000000..b76613df
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
@@ -0,0 +1,47 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import java.lang.annotation.Annotation;
+import java.util.Set;
+
+/**
+ * One named element selection group inside {@link AnnotationElementSpec}.
+ *
+ * An element is selected when it carries one of the {@link #annotationTypes}, or an annotation
+ * type that is itself meta-annotated with {@link #metaMarker} (the only way to discover
+ * user-defined annotation types, since the supported annotation types stay {@code "*"}). Selected
+ * elements carrying the {@link #optOutAnnotation} are dropped again.
+ *
+ * @param name the group name results are keyed by in {@link ElementCollection}
+ * @param annotationTypes annotation types that directly select an element
+ * @param metaMarker meta-annotation marking annotation types that also select, or {@code null}
+ * @param optOutAnnotation annotation that excludes a selected element again, or {@code null}
+ */
+public record ElementSelector(
+ String name,
+ Set> annotationTypes,
+ Class extends Annotation> metaMarker,
+ Class extends Annotation> optOutAnnotation) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index eb2a5b6d..38a2b279 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -56,6 +56,9 @@
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
+import org.javahelpers.codegen.processing.AnnotationElementSpec;
+import org.javahelpers.codegen.processing.ElementCollection;
+import org.javahelpers.codegen.processing.ElementSelector;
import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
@@ -96,6 +99,29 @@
public class BuilderProcessor extends AbstractProcessor {
private static final String MSG_FAILED_TO_GENERATE =
"simple-builders: Failed to generate builder - %s";
+ private static final String GROUP_ANNOTATED_ELEMENTS = "annotatedElements";
+ private static final String GROUP_EXTERNAL_TYPE_HOLDERS = "externalTypeHolders";
+
+ /**
+ * Which elements builders are generated for: types carrying {@code @SimpleBuilder} or a
+ * user-defined {@code @SimpleBuilder.Template}-meta-annotated annotation (opt-out via
+ * {@code @Ignore4BuilderGeneration}), plus classes declaring external types with
+ * {@code @SimpleBuilderFor}.
+ */
+ private static final AnnotationElementSpec ELEMENT_SPEC =
+ new AnnotationElementSpec(
+ List.of(
+ new ElementSelector(
+ GROUP_ANNOTATED_ELEMENTS,
+ Set.of(),
+ Template.class,
+ Ignore4BuilderGeneration.class),
+ new ElementSelector(
+ GROUP_EXTERNAL_TYPE_HOLDERS,
+ Set.of(SimpleBuilderFor.class, SimpleBuilderFors.class),
+ null,
+ null)),
+ PHASE_ELEMENT_COLLECTION);
private BuilderProcessingContext context;
private ProcessingLogger logger;
@@ -177,24 +203,16 @@ public boolean process(Set extends TypeElement> annotations, RoundEnvironment
context.info("simple-builders: PROCESSING ROUND START");
tracker.startPhase();
- Set elementsToProcess = collectElementsToProcess(annotations, roundEnv);
- Set externalTypeHolders = collectExternalTypeHolders(roundEnv);
- // Sort elements alphabetically by simple name for deterministic processing
- List sortedElements =
- elementsToProcess.stream()
- .sorted(Comparator.comparing(element -> element.getSimpleName().toString()))
- .toList();
- List sortedHolders =
- externalTypeHolders.stream()
- .sorted(Comparator.comparing(element -> element.getSimpleName().toString()))
- .toList();
- tracker.endPhase(PHASE_ELEMENT_COLLECTION);
+ ElementCollection collected = ELEMENT_SPEC.collect(annotations, roundEnv, logger);
+ List sortedElements = collected.byName(GROUP_ANNOTATED_ELEMENTS);
+ List sortedHolders = collected.byName(GROUP_EXTERNAL_TYPE_HOLDERS);
+ tracker.endPhase(ELEMENT_SPEC.collectionPhase());
context.debug("simple-builders: Processing round started.");
- context.debug("simple-builders: Found %d annotated elements.", elementsToProcess.size());
+ context.debug("simple-builders: Found %d annotated elements.", sortedElements.size());
context.debug(
"simple-builders: Found %d type(s) for generation with @SimpleBuilderFor.",
- externalTypeHolders.size());
+ sortedHolders.size());
// Resolve configuration and apply generation scopes before processing any builder. This lets
// the scope resolver know every builder that will be generated in this round.
@@ -253,47 +271,6 @@ private void generateJacksonModules(PerformanceTracker tracker) {
context.resetIndentation();
}
- /**
- * Collects all elements annotated with {@code @SimpleBuilderFor} in this round. These are holder
- * classes declaring external types a builder is generated for. Repeated declarations arrive
- * wrapped in the {@code @SimpleBuilderFors} container, so both are collected.
- */
- private Set collectExternalTypeHolders(RoundEnvironment roundEnv) {
- Set holders = new HashSet<>(roundEnv.getElementsAnnotatedWith(SimpleBuilderFor.class));
- holders.addAll(roundEnv.getElementsAnnotatedWith(SimpleBuilderFors.class));
- return holders;
- }
-
- /**
- * Collects all elements to process in this round: any element annotated with an annotation that
- * is meta-annotated with {@code @SimpleBuilder.Template} (including {@code @SimpleBuilder}
- * itself), minus elements opted out via {@code @Ignore4BuilderGeneration}.
- */
- private Set collectElementsToProcess(
- Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
- Set elementsToProcess = new HashSet<>();
- for (TypeElement annotation : extractingAnnotationsWithTemplate(annotations)) {
- elementsToProcess.addAll(roundEnv.getElementsAnnotatedWith(annotation));
- }
-
- // Only directly-declared annotations are checked: the type itself must carry
- // the opt-out; a parent carrying it does not cascade, matching the fact that
- // @Ignore4BuilderGeneration is intentionally NOT @Inherited.
- elementsToProcess.removeIf(
- element -> {
- Optional ignoreAnnotation =
- JavaLangAnalyser.findAnnotation(element, Ignore4BuilderGeneration.class);
- if (ignoreAnnotation.isPresent()) {
- context.debug(
- "Skipping element '%s' due to @Ignore4BuilderGeneration opt-out.",
- element.getSimpleName());
- return true;
- }
- return false;
- });
- return elementsToProcess;
- }
-
/**
* Resolves the configuration per element and applies the {@code builderGenerationPackages} scope,
* returning the elements that will have a builder generated in this round. External types listed
@@ -738,27 +715,4 @@ private static boolean isAtLeastJava17(SourceVersion current) {
return false;
}
}
-
- private static List extractingAnnotationsWithTemplate(
- Set extends TypeElement> annotationsFound) {
- List result = new ArrayList<>();
- for (TypeElement annotation : annotationsFound) {
- if (shouldSkipAnnotation(annotation)) {
- continue;
- }
- Template templateAnnotation =
- annotation.getAnnotation(
- org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template.class);
- if (templateAnnotation != null) {
- result.add(annotation);
- }
- }
- return result;
- }
-
- private static boolean shouldSkipAnnotation(TypeElement annotation) {
- // Only process real annotation specifications. @SimpleBuilder is now a built-in template, so
- // it is processed through the same path as custom template annotations.
- return annotation.getKind() != javax.lang.model.element.ElementKind.ANNOTATION_TYPE;
- }
}
From 1eceaced59b6b7b39ad53ca2b8fe752c63b02f94 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 10:48:12 +0000
Subject: [PATCH 003/170] =?UTF-8?q?processing:=20extract=20ProcessorOption?=
=?UTF-8?q?Registry=20=E2=80=94=20supported=20options=20derived=20from=20d?=
=?UTF-8?q?eclaration,=20not=20duplicated?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/ProcessorOptionRegistry.java | 78 +++++++++++++++++++
.../builders/processor/BuilderProcessor.java | 12 +--
2 files changed, 84 insertions(+), 6 deletions(-)
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
new file mode 100644
index 00000000..48280d3f
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
@@ -0,0 +1,78 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.processing.ProcessingEnvironment;
+import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader;
+import org.javahelpers.simple.builders.processor.processing.CompilerOption;
+
+/**
+ * The compiler options a processor declares, in one place.
+ *
+ * {@code getSupportedOptions()} is derived from the declaration — no hand-duplicated option name
+ * list that can drift from the {@link CompilerOption}s actually parsed.
+ */
+public final class ProcessorOptionRegistry {
+
+ private final List options;
+
+ /**
+ * Creates a registry over the given declared options.
+ *
+ * @param options the processor's compiler options
+ */
+ public ProcessorOptionRegistry(Collection extends CompilerOption> options) {
+ this.options = List.copyOf(options);
+ }
+
+ /**
+ * All supported option names: both the javac-style {@code -A} option name and the legacy
+ * compiler-argument spelling of every declared option.
+ *
+ * @return the supported option names for {@code AbstractProcessor.getSupportedOptions()}
+ */
+ public Set supportedOptions() {
+ Set supported = new HashSet<>();
+ for (CompilerOption option : options) {
+ supported.add(option.getOptionName());
+ supported.add(option.getCompilerArgument());
+ }
+ return supported;
+ }
+
+ /**
+ * A typed reader over the declared options for the given environment.
+ *
+ * @param processingEnv the processing environment to read option values from
+ * @return the option value reader
+ */
+ public CompilerArgumentsReader reader(ProcessingEnvironment processingEnv) {
+ return new CompilerArgumentsReader(processingEnv);
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 38a2b279..c40f9f16 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -60,6 +60,7 @@
import org.javahelpers.codegen.processing.ElementCollection;
import org.javahelpers.codegen.processing.ElementSelector;
import org.javahelpers.codegen.processing.ProcessingTarget;
+import org.javahelpers.codegen.processing.ProcessorOptionRegistry;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;
@@ -102,6 +103,10 @@ public class BuilderProcessor extends AbstractProcessor {
private static final String GROUP_ANNOTATED_ELEMENTS = "annotatedElements";
private static final String GROUP_EXTERNAL_TYPE_HOLDERS = "externalTypeHolders";
+ /** The processor's declared compiler options — the supported set is derived, not duplicated. */
+ private static final ProcessorOptionRegistry OPTION_REGISTRY =
+ new ProcessorOptionRegistry(List.of(CompilerArgumentsEnum.values()));
+
/**
* Which elements builders are generated for: types carrying {@code @SimpleBuilder} or a
* user-defined {@code @SimpleBuilder.Template}-meta-annotated annotation (opt-out via
@@ -574,12 +579,7 @@ private int generateBuilders(
@Override
public Set getSupportedOptions() {
- Set options = new HashSet<>();
- for (CompilerArgumentsEnum arg : CompilerArgumentsEnum.values()) {
- options.add(arg.getOptionName()); // e.g., "verbose"
- options.add(arg.getCompilerArgument()); // e.g., "simplebuilder.verbose"
- }
- return options;
+ return OPTION_REGISTRY.supportedOptions();
}
@Override
From 7fcd807d85137f72237ae7732d104d679ad2369d Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 10:49:06 +0000
Subject: [PATCH 004/170] =?UTF-8?q?processing:=20generalize=20SourceFormat?=
=?UTF-8?q?terResolver=20=E2=80=94=20profile=20and=20formatter=20factory?=
=?UTF-8?q?=20injected,=20no=20roaster/option=20coupling?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/SourceFormatterResolver.java | 41 ++++++++++++-------
.../builders/processor/BuilderProcessor.java | 10 ++++-
2 files changed, 34 insertions(+), 17 deletions(-)
rename processor/src/main/java/org/javahelpers/{simple/builders/processor => codegen}/processing/SourceFormatterResolver.java (63%)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java b/processor/src/main/java/org/javahelpers/codegen/processing/SourceFormatterResolver.java
similarity index 63%
rename from processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java
rename to processor/src/main/java/org/javahelpers/codegen/processing/SourceFormatterResolver.java
index 92bb00ad..d0eab969 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/SourceFormatterResolver.java
@@ -22,41 +22,53 @@
* SOFTWARE.
*/
-package org.javahelpers.simple.builders.processor.processing;
+package org.javahelpers.codegen.processing;
import java.util.EnumMap;
-import javax.annotation.processing.ProcessingEnvironment;
-import org.apache.commons.lang3.StringUtils;
import org.javahelpers.simple.builders.core.enums.FormattingMode;
import org.javahelpers.simple.builders.processor.classgen.SourceFormatter;
-import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterSourceFormatter;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
/**
- * Resolves the {@link SourceFormatter} for a formatting mode, applying the Eclipse formatter
- * profile configured via the {@code formatterProfile} compiler argument.
+ * Resolves the {@link SourceFormatter} for a formatting mode, applying the formatter profile the
+ * domain configured.
*
* At most one formatter instance per {@link FormattingMode} is created, lazily on first resolve.
+ * The concrete formatter implementation is supplied by the domain — the processing machinery only
+ * knows the {@link SourceFormatter} contract.
*/
public class SourceFormatterResolver {
+ /** Creates a {@link SourceFormatter} for a mode under a formatter profile. */
+ @FunctionalInterface
+ public interface FormatterFactory {
+ /**
+ * @param logger logger for formatter warnings
+ * @param mode the formatting mode
+ * @param profile the formatter profile path, or {@code null} for the built-in profile
+ * @return the formatter to use
+ */
+ SourceFormatter create(ProcessingLogger logger, FormattingMode mode, String profile);
+ }
+
private final ProcessingLogger logger;
private final String formatterProfile;
+ private final FormatterFactory factory;
private final EnumMap formatterCache =
new EnumMap<>(FormattingMode.class);
/**
- * Creates a resolver reading the formatter profile from compiler arguments.
+ * Creates a resolver.
*
- * @param processingEnv the processing environment (for compiler argument access)
* @param logger logger for formatter warnings
+ * @param formatterProfile the configured formatter profile path, or {@code null}
+ * @param factory creates the formatter per mode
*/
- public SourceFormatterResolver(ProcessingEnvironment processingEnv, ProcessingLogger logger) {
+ public SourceFormatterResolver(
+ ProcessingLogger logger, String formatterProfile, FormatterFactory factory) {
this.logger = logger;
- this.formatterProfile =
- StringUtils.trimToNull(
- new CompilerArgumentsReader(processingEnv)
- .readValue(CompilerArgumentsEnum.FORMATTER_PROFILE));
+ this.formatterProfile = formatterProfile;
+ this.factory = factory;
}
/**
@@ -66,7 +78,6 @@ public SourceFormatterResolver(ProcessingEnvironment processingEnv, ProcessingLo
* @return a cached or new formatter instance
*/
public SourceFormatter resolve(FormattingMode mode) {
- return formatterCache.computeIfAbsent(
- mode, m -> new RoasterSourceFormatter(logger, m, formatterProfile));
+ return formatterCache.computeIfAbsent(mode, m -> factory.create(logger, m, formatterProfile));
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index c40f9f16..98ac5a29 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -56,11 +56,13 @@
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
+import org.apache.commons.lang3.StringUtils;
import org.javahelpers.codegen.processing.AnnotationElementSpec;
import org.javahelpers.codegen.processing.ElementCollection;
import org.javahelpers.codegen.processing.ElementSelector;
import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.codegen.processing.ProcessorOptionRegistry;
+import org.javahelpers.codegen.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;
@@ -72,6 +74,7 @@
import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException;
import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator;
+import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterSourceFormatter;
import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
@@ -86,7 +89,6 @@
import org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext;
import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsEnum;
import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader;
-import org.javahelpers.simple.builders.processor.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
@@ -160,7 +162,11 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
logger,
context.getPerformanceTracker(),
context::reportBasedOnStrictMode));
- this.formatterResolver = new SourceFormatterResolver(processingEnv, logger);
+ this.formatterResolver =
+ new SourceFormatterResolver(
+ logger,
+ StringUtils.trimToNull(reader.readValue(CompilerArgumentsEnum.FORMATTER_PROFILE)),
+ RoasterSourceFormatter::new);
this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig);
// Initialize GeneratorRegistry once during processor initialization
From 903e63e3640cf82adbb226763627f06b61ecd784 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 10:50:41 +0000
Subject: [PATCH 005/170] =?UTF-8?q?processing:=20hoist=20GeneratedTypes=20?=
=?UTF-8?q?registry=20onto=20AnnotationProcessingContext=20=E2=80=94=20dri?=
=?UTF-8?q?ver=20and=20domain=20share=20former-generated=20recognition?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../analysis/BuilderScopeResolver.java | 4 +++-
.../AnnotationProcessingContext.java | 18 ++++++++++++++++++
2 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
index 85b5d7a1..34222b1d 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
@@ -79,7 +79,8 @@ private record ResolutionInputs(BuilderConfiguration configuration, String build
private ResolutionInputs cachedResolutionInputs;
private PackageScopes usagePackages = PackageScopes.unscoped();
private final Map> resolvedBuilderTypes = new HashMap<>();
- private final GeneratedTypes generatedBuilders = new GeneratedTypes();
+ // Generated-types registry lives on the context (shared with the processing driver)
+ private final GeneratedTypes generatedBuilders;
/**
* Creates a new resolver for the given processing context.
@@ -88,6 +89,7 @@ private record ResolutionInputs(BuilderConfiguration configuration, String build
*/
public BuilderScopeResolver(BuilderProcessingContext context) {
this.context = context;
+ this.generatedBuilders = context.getGeneratedTypes();
}
/**
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
index 608d9f99..c7182a5b 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
@@ -39,6 +39,7 @@
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
+import org.javahelpers.simple.builders.processor.analysis.GeneratedTypes;
import org.javahelpers.simple.builders.processor.analysis.TypeMappingContext;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.logging.ActivePerformanceTracker;
@@ -73,6 +74,12 @@ public class AnnotationProcessingContext implements TypeMappingContext {
/** Static options supplied by the concrete context. */
protected final AnnotationProcessingOptions options;
+ /**
+ * Registry of the types generated in the current processing round — the up-front recognition of
+ * former (and planned) generated classes.
+ */
+ private final GeneratedTypes generatedTypes = new GeneratedTypes();
+
/** Package generated code is written to, or {@code null} when not generating into a package. */
private String generatedPackageName;
@@ -207,6 +214,17 @@ public Optional resolveGeneratedType(TypeElement typeElement) {
return Optional.empty();
}
+ /**
+ * The registry of types generated in the current processing round. Domains register planned
+ * outputs before generating, so references to them resolve; downstream code queries via {@link
+ * GeneratedTypes#findGenerated}.
+ *
+ * @return the generated-types registry
+ */
+ public GeneratedTypes getGeneratedTypes() {
+ return generatedTypes;
+ }
+
/**
* Get the TypeElement for a given qualified class name.
*
From b3b39e3a3d29910a1374625b32f53dc378fc3259 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 11:01:07 +0000
Subject: [PATCH 006/170] =?UTF-8?q?processing:=20introduce=20AbstractCodeg?=
=?UTF-8?q?enProcessor=20=E2=80=94=20generic=20collect-plan-analyse-map-ge?=
=?UTF-8?q?nerate=20driver;=20BuilderProcessor=20collapses=20to=20domain?=
=?UTF-8?q?=20hooks?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 366 ++++++++++++++
.../codegen/processing/CodegenTask.java | 42 ++
.../builders/processor/BuilderProcessor.java | 459 +++++++-----------
.../processor/BuilderScopeResolverTest.java | 2 +-
.../RoasterCodeGeneratorResilienceTest.java | 9 +-
5 files changed, 601 insertions(+), 277 deletions(-)
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
new file mode 100644
index 00000000..99b7748b
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -0,0 +1,366 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import java.util.List;
+import java.util.Set;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.TypeElement;
+import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
+import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException;
+import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
+import org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext;
+import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader;
+import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker;
+import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
+
+/**
+ * Driver of a generic annotation-processing pipeline: the framework owns the processing lifecycle,
+ * the domain plugs in its hooks — collect, plan, register, analyse/map, generate, finalize.
+ *
+ * The round spine:
+ *
+ *
+ * ctx = domain.createContext(env, logger) [in init]
+ * elements = elementSpec().collect(annotations, roundEnv, logger)
+ * tasks = domain.plan(elements, ctx)
+ * domain.registerPlannedOutputs(tasks, ctx) [up-front type recognition]
+ * for each task (guarded, strict-mode reporting):
+ * def = domain.analyse(task, ctx)
+ * targets = domain.mapToTargets(def, task, ctx)
+ * for each target:
+ * existsGuard → engine.generate(target, formatterResolver.resolve(target.mode))
+ * domain.afterGenerate(def, targets, task, ctx)
+ * domain.endRound(ctx)
+ * last round: domain.onProcessingOver(ctx) [deferred outputs, reports]
+ *
+ *
+ * Per-element failures are caught and reported through {@link
+ * AnnotationProcessingContext#reportBasedOnStrictMode}, so one broken element does not abort the
+ * round; in strict mode they become errors.
+ *
+ * @param the domain's processing context type
+ * @param the domain's configuration type carried by {@link CodegenTask}
+ * @param the domain's definition type — the analysis result mapped to render targets
+ */
+public abstract class AbstractCodegenProcessor
+ extends AbstractProcessor {
+
+ /** The domain's context instance. */
+ protected C context;
+
+ /** Logger for the domain's hooks (init, last-round reports). */
+ protected ProcessingLogger logger;
+
+ private ClassCodeGenerator codeGenerator;
+ private SourceFormatterResolver formatterResolver;
+ private boolean supportedJdk = true;
+
+ @Override
+ public synchronized void init(ProcessingEnvironment processingEnv) {
+ super.init(processingEnv);
+ CompilerArgumentsReader reader = new CompilerArgumentsReader(processingEnv);
+ this.logger = createLogger(processingEnv, reader);
+ this.context = createContext(processingEnv, reader, logger);
+ this.codeGenerator = createGenerator(processingEnv, logger, context);
+ this.formatterResolver = createFormatterResolver(reader, logger);
+ onInit(processingEnv, reader, context);
+
+ SourceVersion current = processingEnv.getSourceVersion();
+ this.supportedJdk = isAtLeast(current, requiredSourceVersion());
+ if (!this.supportedJdk) {
+ String releaseNumber = requiredSourceVersion().replace("RELEASE_", "");
+ context.error(
+ "%s requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor.",
+ processorTag(), releaseNumber, current, releaseNumber);
+ }
+ }
+
+ /**
+ * Runs one processing round — see the class javadoc for the spine. Never claims annotations:
+ * claiming is all-or-nothing over the supported set and {@link SupportedAnnotationTypes} must
+ * stay {@code "*"} to discover user-defined (meta-annotated) annotation types; claiming would
+ * hide foreign annotations from later processors (hence the {@code java:S3516} suppression).
+ */
+ @Override
+ @SuppressWarnings("java:S3516")
+ public final boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ if (!supportedJdk) {
+ // Fail fast: init() already emitted the error; do not attempt any processing.
+ return false;
+ }
+ if (roundEnv.processingOver()) {
+ onProcessingOver(context);
+ return false;
+ }
+
+ PerformanceTracker tracker = context.getPerformanceTracker();
+ context.info("%s: PROCESSING ROUND START", processorTag());
+
+ tracker.startPhase();
+ ElementCollection collected = elementSpec().collect(annotations, roundEnv, logger);
+ tracker.endPhase(elementSpec().collectionPhase());
+ context.debug("%s: Processing round started.", processorTag());
+
+ List> tasks = plan(collected, context);
+ if (collected.isEmpty() || isFinished(context)) {
+ return false;
+ }
+
+ registerPlannedOutputs(tasks, context);
+
+ int successfulGenerations = 0;
+ for (CodegenTask task : tasks) {
+ context.debugStartOperation("Processing element: " + task.element().getSimpleName());
+ tracker.startClass(task.element().getSimpleName().toString());
+ try {
+ D definition = analyse(task, context);
+ List targets = mapToTargets(definition, task, context);
+ tracker.startPhase();
+ for (GenerationTargetClassDto target : targets) {
+ generateTarget(target);
+ }
+ tracker.endPhase(generationPhase());
+ afterGenerate(definition, targets, task, context);
+ successfulGenerations++;
+ } catch (Exception ex) {
+ // By default generation failures are warnings so other targets are still generated.
+ // In opt-in strict mode they are promoted to errors that fail the build.
+ context.reportBasedOnStrictMode(
+ task.reportingElement(),
+ "%s: Failed to generate %s - %s",
+ processorTag(),
+ generatedKind(),
+ ex.getMessage());
+ } finally {
+ context.debugEndOperation();
+ }
+ }
+
+ if (successfulGenerations > 0) {
+ context.info(
+ "%s: Successfully generated %d %s(s) in this processing round",
+ processorTag(), successfulGenerations, generatedKind());
+ }
+
+ // Reset indentation level at the end of each processing round to prevent cascading errors
+ context.resetIndentation();
+ endRound(context);
+ return false;
+ }
+
+ @Override
+ public final Set getSupportedOptions() {
+ return optionRegistry().supportedOptions();
+ }
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latestSupported();
+ }
+
+ /**
+ * Generates one target class through the configured engine after checking the target does not
+ * already exist. Also usable by the domain's {@link #onProcessingOver} implementation for
+ * deferred outputs.
+ *
+ * @param target the class definition to generate
+ * @throws CodeGenerationException if the target exists or generation fails
+ */
+ protected final void generateTarget(GenerationTargetClassDto target)
+ throws CodeGenerationException {
+ assertTargetDoesNotExist(target);
+ codeGenerator.generateClass(target, formatterResolver.resolve(target.getFormattingMode()));
+ }
+
+ /**
+ * Fails the build with a descriptive error when the class to be generated already exists (a
+ * manually written class or a leftover from a previous compilation).
+ *
+ * @param target the class definition to check
+ * @throws CodeGenerationException if a class with the target qualified name already exists
+ */
+ private void assertTargetDoesNotExist(GenerationTargetClassDto target)
+ throws CodeGenerationException {
+ String qualifiedName = target.getTypeName().getFullQualifiedName();
+ boolean exists;
+ try {
+ exists = context.getTypeElement(qualifiedName) != null;
+ } catch (Exception e) {
+ context.debug(
+ "Error checking if class '%s' already exists: %s",
+ qualifiedName, e.getMessage() != null ? e.getMessage() : "No message");
+ exists = false;
+ }
+ if (exists) {
+ String kind = generatedKind();
+ String capitalized = Character.toUpperCase(kind.charAt(0)) + kind.substring(1);
+ throw new CodeGenerationException(
+ """
+ %s class '%s' already exists. This may be a manually written %s or a previously generated %s.
+ To resolve this:
+ 1. If you have a manual %s, consider renaming it or removing the annotation triggering generation
+ 2. If this is from a previous compilation, clean and rebuild the project
+ 3. Check that you're not trying to generate multiple %ss for the same target type
+ """
+ .formatted(capitalized, qualifiedName, kind, kind, kind, kind));
+ }
+ }
+
+ /**
+ * Checks whether the current SourceVersion is at least the required one, in a backwards
+ * compatible way (the required constant may not exist on older JDKs).
+ */
+ private static boolean isAtLeast(SourceVersion current, String requiredName) {
+ try {
+ return current.ordinal() >= SourceVersion.valueOf(requiredName).ordinal();
+ } catch (IllegalArgumentException ex) {
+ // Running on a JDK where the required release does not exist
+ return false;
+ }
+ }
+
+ /** Creates the logger honoring the domain's verbosity option. */
+ protected abstract ProcessingLogger createLogger(
+ ProcessingEnvironment processingEnv, CompilerArgumentsReader reader);
+
+ /**
+ * Creates the domain's processing context.
+ *
+ * @param processingEnv the processing environment
+ * @param reader compiler-argument access — global configuration typically comes from here
+ * @param logger logger for output
+ * @return the domain's context instance
+ */
+ protected abstract C createContext(
+ ProcessingEnvironment processingEnv, CompilerArgumentsReader reader, ProcessingLogger logger);
+
+ /** Creates the code generation engine — the {@link ClassCodeGenerator} backend port. */
+ protected abstract ClassCodeGenerator createGenerator(
+ ProcessingEnvironment processingEnv, ProcessingLogger logger, C context);
+
+ /** Creates the source formatter resolver for the configured profile. */
+ protected abstract SourceFormatterResolver createFormatterResolver(
+ CompilerArgumentsReader reader, ProcessingLogger logger);
+
+ /** Which elements the domain generates code for. */
+ protected abstract AnnotationElementSpec elementSpec();
+
+ /** The processor's declared compiler options. */
+ protected abstract ProcessorOptionRegistry optionRegistry();
+
+ /** The minimum source level the processor supports, as a {@link SourceVersion} name. */
+ protected abstract String requiredSourceVersion();
+
+ /** Diagnostic tag prefixing messages, e.g. {@code "simple-builders"}. */
+ protected abstract String processorTag();
+
+ /** What one generated unit is called in diagnostics, e.g. {@code "builder"}. */
+ protected abstract String generatedKind();
+
+ /** Domain-specific initialization, after the framework wiring is in place. */
+ protected void onInit(
+ ProcessingEnvironment processingEnv, CompilerArgumentsReader reader, C context) {}
+
+ /**
+ * Plans the tasks of this round from the collected elements: resolves per-element configuration,
+ * expands multi-target declarations and applies domain scoping rules.
+ *
+ * @param collected the elements matched by {@link #elementSpec()}
+ * @param context the processing context
+ * @return the generation plan, possibly empty
+ */
+ protected abstract List> plan(ElementCollection collected, C context);
+
+ /**
+ * Registers the types this round will generate, up front, so references to them resolve during
+ * analysis. Domains register into {@link AnnotationProcessingContext#getGeneratedTypes()}.
+ * Default: no registration.
+ *
+ * @param tasks the planned tasks
+ * @param context the processing context
+ */
+ protected void registerPlannedOutputs(List> tasks, C context) {}
+
+ /**
+ * Whether the domain generates in exactly one round and that round already ran. Default {@code
+ * false} — every round with matching elements is processed.
+ *
+ * @param context the processing context
+ * @return {@code true} to skip this round
+ */
+ protected boolean isFinished(C context) {
+ return false;
+ }
+
+ /**
+ * Analyses one planned task into its domain definition — the analysis half of the
+ * analysis/generation separation.
+ *
+ * @param task the planned task
+ * @param context the processing context
+ * @return the domain definition
+ * @throws Exception when analysis fails; reported as a per-element failure
+ */
+ protected abstract D analyse(CodegenTask task, C context) throws Exception;
+
+ /**
+ * Maps a domain definition to render targets — the generation half of the analysis/generation
+ * separation, separated by the {@link GenerationTargetClassDto} boundary.
+ *
+ * @param definition the analysed definition
+ * @param task the planned task
+ * @param context the processing context
+ * @return the class definitions to generate (usually one)
+ * @throws Exception when mapping fails; reported as a per-element failure
+ */
+ protected abstract List mapToTargets(
+ D definition, CodegenTask task, C context) throws Exception;
+
+ /**
+ * Runs after all targets of a task generated successfully — e.g. deferred-output accumulation and
+ * per-class metrics. Default: nothing.
+ *
+ * @param definition the analysed definition
+ * @param targets the generated class definitions
+ * @param task the planned task
+ * @param context the processing context
+ */
+ protected void afterGenerate(
+ D definition, List targets, CodegenTask task, C context) {}
+
+ /** The performance phase wrapping target generation. */
+ protected abstract String generationPhase();
+
+ /** Callback for the last processing round — deferred outputs and final reports. */
+ protected void onProcessingOver(C context) {}
+
+ /** Callback at the end of a generating round — domain finalization. */
+ protected void endRound(C context) {}
+}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
new file mode 100644
index 00000000..70e33f62
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
@@ -0,0 +1,42 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import javax.lang.model.element.Element;
+
+/**
+ * A planned generation: one element to generate code for, with the configuration the domain
+ * resolved during planning.
+ *
+ * One round element may expand into several tasks (e.g. an annotation declaring multiple
+ * external types).
+ *
+ * @param the domain's configuration type
+ * @param element the element code is generated for
+ * @param configuration the resolved per-element configuration
+ * @param reportingElement the element diagnostics are reported on — the declaring holder for
+ * externally declared types, otherwise the element itself
+ */
+public record CodegenTask(Element element, K configuration, Element reportingElement) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 98ac5a29..5584180b 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -41,12 +41,9 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
-import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
-import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
-import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
@@ -57,7 +54,9 @@
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import org.apache.commons.lang3.StringUtils;
+import org.javahelpers.codegen.processing.AbstractCodegenProcessor;
import org.javahelpers.codegen.processing.AnnotationElementSpec;
+import org.javahelpers.codegen.processing.CodegenTask;
import org.javahelpers.codegen.processing.ElementCollection;
import org.javahelpers.codegen.processing.ElementSelector;
import org.javahelpers.codegen.processing.ProcessingTarget;
@@ -94,14 +93,14 @@
/**
* BuilderProcessor is an annotation processor for execution in generate-sources phase. The
- * BuilderProcessor using the Java way for generating builders, by implementing {@code
- * javax.annotation.processing.AbstractProcessor}.
+ * BuilderProcessor using the Java way for generating builders, plugged into the generic pipeline
+ * driven by {@link AbstractCodegenProcessor}.
*/
@AutoService(Processor.class)
@SupportedAnnotationTypes("*")
-public class BuilderProcessor extends AbstractProcessor {
- private static final String MSG_FAILED_TO_GENERATE =
- "simple-builders: Failed to generate builder - %s";
+public class BuilderProcessor
+ extends AbstractCodegenProcessor<
+ BuilderProcessingContext, BuilderConfiguration, BuilderDefinitionDto> {
private static final String GROUP_ANNOTATED_ELEMENTS = "annotatedElements";
private static final String GROUP_EXTERNAL_TYPE_HOLDERS = "externalTypeHolders";
@@ -130,170 +129,137 @@ public class BuilderProcessor extends AbstractProcessor {
null)),
PHASE_ELEMENT_COLLECTION);
- private BuilderProcessingContext context;
- private ProcessingLogger logger;
- private ClassCodeGenerator codeGenerator;
- private SourceFormatterResolver formatterResolver;
+ /** The global configuration resolved from compiler arguments — needed by several hooks. */
+ private BuilderConfiguration globalConfig;
+
private JacksonModuleGenerator jacksonModuleGenerator;
- private boolean supportedJdk = true;
@Override
- public synchronized void init(ProcessingEnvironment processingEnv) {
- super.init(processingEnv);
- // Read global configuration from compiler arguments
- CompilerArgumentsReader reader = new CompilerArgumentsReader(processingEnv);
- this.logger =
+ protected ProcessingLogger createLogger(
+ ProcessingEnvironment processingEnv, CompilerArgumentsReader reader) {
+ ProcessingLogger logger =
reader.readBooleanValue(CompilerArgumentsEnum.VERBOSE)
? ProcessingLogger.createVerbose(processingEnv)
: ProcessingLogger.create(processingEnv);
logger.debug("Starting BuilderProcessor...");
+ return logger;
+ }
- BuilderConfiguration globalConfig =
- BuilderConfigurationReader.readCompilerConfiguration(reader, logger);
+ @Override
+ protected BuilderProcessingContext createContext(
+ ProcessingEnvironment processingEnv,
+ CompilerArgumentsReader reader,
+ ProcessingLogger logger) {
+ this.globalConfig = BuilderConfigurationReader.readCompilerConfiguration(reader, logger);
logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig);
-
SimpleBuildersSpiIntegration.initCompilation(processingEnv.getElementUtils());
+ return new BuilderProcessingContext(logger, globalConfig, processingEnv);
+ }
+
+ @Override
+ protected ClassCodeGenerator createGenerator(
+ ProcessingEnvironment processingEnv, ProcessingLogger logger, BuilderProcessingContext ctx) {
+ return new RoasterCodeGenerator(
+ new GenerationEnvironment(
+ processingEnv, logger, ctx.getPerformanceTracker(), ctx::reportBasedOnStrictMode));
+ }
- this.context = new BuilderProcessingContext(logger, globalConfig, processingEnv);
- this.codeGenerator =
- new RoasterCodeGenerator(
- new GenerationEnvironment(
- processingEnv,
- logger,
- context.getPerformanceTracker(),
- context::reportBasedOnStrictMode));
- this.formatterResolver =
- new SourceFormatterResolver(
- logger,
- StringUtils.trimToNull(reader.readValue(CompilerArgumentsEnum.FORMATTER_PROFILE)),
- RoasterSourceFormatter::new);
+ @Override
+ protected SourceFormatterResolver createFormatterResolver(
+ CompilerArgumentsReader reader, ProcessingLogger logger) {
+ return new SourceFormatterResolver(
+ logger,
+ StringUtils.trimToNull(reader.readValue(CompilerArgumentsEnum.FORMATTER_PROFILE)),
+ RoasterSourceFormatter::new);
+ }
+
+ @Override
+ protected void onInit(
+ ProcessingEnvironment processingEnv,
+ CompilerArgumentsReader reader,
+ BuilderProcessingContext ctx) {
this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig);
// Initialize GeneratorRegistry once during processor initialization
- context.debugStartOperation("Initializing generator registry");
+ ctx.debugStartOperation("Initializing generator registry");
try {
- context.getGeneratorRegistry();
+ ctx.getGeneratorRegistry();
} finally {
- context.debugEndOperation();
+ ctx.debugEndOperation();
}
+ }
- SourceVersion current = processingEnv.getSourceVersion();
- this.supportedJdk = isAtLeastJava17(current);
- if (!this.supportedJdk) {
- context.error(
- "simple-builders requires Java 17 or higher for annotation processing. Detected: '%s'. Please upgrade to JDK 17+ or disable the processor.",
- current);
- }
+ @Override
+ protected AnnotationElementSpec elementSpec() {
+ return ELEMENT_SPEC;
}
- /**
- * Generates all builders in exactly one round — the first round carrying simple-builders
- * annotations — and marks the SPI registry final when that round ends.
- *
- * Always returns {@code false}, so this method intentionally never claims annotations (hence
- * the {@code java:S3516} suppression). Claiming is all-or-nothing over the supported set and
- * {@link SupportedAnnotationTypes} must stay {@code "*"} to discover user-defined template
- * annotations; claiming would therefore hide every annotation in the round — including foreign
- * ones like MapStruct's {@code @Mapper} — from later processors.
- */
@Override
- @SuppressWarnings("java:S3516")
- public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
- if (!supportedJdk) {
- // Fail fast: we already emitted an error in init(); do not attempt any processing.
- return false;
- }
+ protected ProcessorOptionRegistry optionRegistry() {
+ return OPTION_REGISTRY;
+ }
- // Generate Jackson Module if processing is over and feature is enabled
- if (roundEnv.processingOver()) {
- SimpleBuildersSpiIntegration.finishCompilation();
- generateJacksonModules(context.getPerformanceTracker());
- return false;
- }
- PerformanceTracker tracker = context.getPerformanceTracker();
- context.info("simple-builders: PROCESSING ROUND START");
+ @Override
+ protected String requiredSourceVersion() {
+ return "RELEASE_17";
+ }
- tracker.startPhase();
- ElementCollection collected = ELEMENT_SPEC.collect(annotations, roundEnv, logger);
+ @Override
+ protected String processorTag() {
+ return "simple-builders";
+ }
+
+ @Override
+ protected String generatedKind() {
+ return "builder";
+ }
+
+ @Override
+ protected String generationPhase() {
+ return PHASE_CODE_GENERATION;
+ }
+
+ @Override
+ protected List> plan(
+ ElementCollection collected, BuilderProcessingContext ctx) {
List sortedElements = collected.byName(GROUP_ANNOTATED_ELEMENTS);
List sortedHolders = collected.byName(GROUP_EXTERNAL_TYPE_HOLDERS);
- tracker.endPhase(ELEMENT_SPEC.collectionPhase());
-
- context.debug("simple-builders: Processing round started.");
- context.debug("simple-builders: Found %d annotated elements.", sortedElements.size());
- context.debug(
+ ctx.debug("simple-builders: Found %d annotated elements.", sortedElements.size());
+ ctx.debug(
"simple-builders: Found %d type(s) for generation with @SimpleBuilderFor.",
sortedHolders.size());
-
- // Resolve configuration and apply generation scopes before processing any builder. This lets
- // the scope resolver know every builder that will be generated in this round.
- List elementsToGenerate =
+ List> tasks =
resolveGenerationPlan(
- sortedElements, sortedHolders, context.getConfigurationReader(), tracker);
- context.debug(
+ sortedElements,
+ sortedHolders,
+ ctx.getConfigurationReader(),
+ ctx.getPerformanceTracker());
+ ctx.debug(
"simple-builders: %d of %d annotated element(s) are inside the builderGenerationPackages scope.",
- elementsToGenerate.size(), sortedElements.size());
- if (SimpleBuildersSpiIntegration.isSimpleBuildersFinishedForIntegration(
- processingEnv.getElementUtils())
- || (sortedElements.isEmpty() && sortedHolders.isEmpty())) {
- // simple-builders generates in exactly one round — the first round carrying its
- // annotations; elements first appearing in later rounds get no builder.
- return false;
- }
- registerGeneratedTypes(elementsToGenerate);
-
- int successfulGenerations = generateBuilders(elementsToGenerate, tracker);
-
- // Log summary of builder generation
- if (successfulGenerations > 0) {
- context.info(
- "simple-builders: Successfully generated %d builder(s) in this processing round",
- successfulGenerations);
- }
-
- // Reset indentation level at the end of each processing round to prevent cascading errors
- context.resetIndentation();
- // The generating round is done — the registry is final for SPI adapters from here on.
- SimpleBuildersSpiIntegration.finishCompilation();
- return false;
+ tasks.size(), sortedElements.size());
+ return tasks;
}
- /** Generates all Jackson modules after the last processing round and reports the metrics. */
- private void generateJacksonModules(PerformanceTracker tracker) {
- tracker.generateReport(logger);
-
- List moduleClassDefs = jacksonModuleGenerator.getModuleDefinitions();
- for (GenerationTargetClassDto moduleClassDef : moduleClassDefs) {
- String packageName = moduleClassDef.getTypeName().getPackageName();
- context.info("Generating Jackson Module in package '%s'", packageName);
- try {
- assertTargetClassDoesNotExist(moduleClassDef);
- codeGenerator.generateClass(
- moduleClassDef, formatterResolver.resolve(moduleClassDef.getFormattingMode()));
- } catch (CodeGenerationException e) {
- // By default Jackson module generation failures are warnings. In opt-in strict mode
- // they are promoted to errors that fail the build.
- context.reportBasedOnStrictMode(
- "simple-builders: Error generating Jackson module for package %s: %s",
- packageName, e.getMessage());
- }
- }
- // Reset indentation after Jackson module generation as well
- context.resetIndentation();
+ @Override
+ protected boolean isFinished(BuilderProcessingContext ctx) {
+ // simple-builders generates in exactly one round — the first round carrying its
+ // annotations; elements first appearing in later rounds get no builder.
+ return SimpleBuildersSpiIntegration.isSimpleBuildersFinishedForIntegration(
+ processingEnv.getElementUtils());
}
/**
* Resolves the configuration per element and applies the {@code builderGenerationPackages} scope,
- * returning the elements that will have a builder generated in this round. External types listed
- * in {@code @SimpleBuilderFor} are expanded afterwards so a directly annotated DTO always wins
- * over an external-type request for the same builder name.
+ * returning the tasks for this round. External types listed in {@code @SimpleBuilderFor} are
+ * expanded afterwards so a directly annotated DTO always wins over an external-type request for
+ * the same builder name.
*/
- private List resolveGenerationPlan(
+ private List> resolveGenerationPlan(
List sortedElements,
List sortedHolders,
BuilderConfigurationReader reader,
PerformanceTracker tracker) {
- List elementsToGenerate = new ArrayList<>();
+ List> tasks = new ArrayList<>();
// Builder names already planned in this round: needed for conflict detection, so a directly
// annotated type wins over an external-type declaration producing the same builder name
// (the conflicting @SimpleBuilderFor entry is skipped with a warning).
@@ -302,11 +268,12 @@ private List resolveGenerationPlan(
context.debugStartOperation("Processing element: " + annotatedElement.getSimpleName());
try {
planGenerationOfAnnotatedElement(annotatedElement, reader, alreadyPlannedBuilders, tracker)
- .ifPresent(elementsToGenerate::add);
+ .ifPresent(tasks::add);
} catch (BuilderException ex) {
// By default builder generation failures are warnings so other builders are still
// generated. In opt-in strict mode they are promoted to errors that fail the build.
- context.reportBasedOnStrictMode(annotatedElement, MSG_FAILED_TO_GENERATE, ex.getMessage());
+ context.reportBasedOnStrictMode(
+ annotatedElement, "simple-builders: Failed to generate builder - %s", ex.getMessage());
} finally {
context.debugEndOperation();
}
@@ -315,22 +282,22 @@ private List resolveGenerationPlan(
for (Element holder : sortedHolders) {
context.debugStartOperation("Processing @SimpleBuilderFor holder: " + holder.getSimpleName());
try {
- elementsToGenerate.addAll(
- planGenerationOfTypeByHolder(holder, reader, alreadyPlannedBuilders, tracker));
+ tasks.addAll(planGenerationOfTypeByHolder(holder, reader, alreadyPlannedBuilders, tracker));
} catch (BuilderException ex) {
- context.reportBasedOnStrictMode(holder, MSG_FAILED_TO_GENERATE, ex.getMessage());
+ context.reportBasedOnStrictMode(
+ holder, "simple-builders: Failed to generate builder - %s", ex.getMessage());
} finally {
context.debugEndOperation();
}
}
- return elementsToGenerate;
+ return tasks;
}
/**
* Resolves the configuration of one annotated element and plans its builder, or returns empty
* when the element is outside the {@code builderGenerationPackages} scope.
*/
- private Optional planGenerationOfAnnotatedElement(
+ private Optional> planGenerationOfAnnotatedElement(
Element annotatedElement,
BuilderConfigurationReader reader,
Set alreadyPlannedBuilders,
@@ -346,7 +313,7 @@ private Optional planGenerationOfAnnotatedElement(
alreadyPlannedBuilders.add(
builderTypeName(
annotatedElement, effectiveBuilderPackage(annotatedElement, config), config));
- return Optional.of(new ElementToGenerate(annotatedElement, config, annotatedElement));
+ return Optional.of(new CodegenTask<>(annotatedElement, config, annotatedElement));
}
/**
@@ -356,7 +323,7 @@ private Optional planGenerationOfAnnotatedElement(
* into different packages via {@code packageName}; without a configured package the holder's
* package is used.
*/
- private List planGenerationOfTypeByHolder(
+ private List> planGenerationOfTypeByHolder(
Element holder,
BuilderConfigurationReader reader,
Set alreadyPlannedBuilders,
@@ -367,7 +334,7 @@ private List planGenerationOfTypeByHolder(
JavaLangAnalyser.findAnnotationInstances(
holder, SimpleBuilderFor.class, SimpleBuilderFors.class, context);
- List results = new ArrayList<>();
+ List> results = new ArrayList<>();
for (AnnotationMirror annotationInstance : annotationInstances) {
List targets = extractExternalTargetTypes(holder, annotationInstance);
if (targets.isEmpty()) {
@@ -398,7 +365,7 @@ private List planGenerationOfTypeByHolder(
* and the {@code builderGenerationPackages} scope only filters annotated types, so a scope that
* would exclude an explicitly named type is a contradictory configuration and only warns.
*/
- private Optional planExternalTarget(
+ private Optional> planExternalTarget(
TypeElement target,
Element holder,
BuilderConfiguration config,
@@ -423,7 +390,7 @@ private Optional planExternalTarget(
builderName.getFullQualifiedName());
return Optional.empty();
}
- return Optional.of(new ElementToGenerate(target, config, holder));
+ return Optional.of(new CodegenTask<>(target, config, holder));
}
/**
@@ -536,109 +503,66 @@ private TypeName builderTypeName(
* can trust them without a type search. The actual builder type name is registered, which may
* differ from the target's package for {@code @SimpleBuilderFor} targets.
*/
- private void registerGeneratedTypes(List elementsToGenerate) {
- BuilderScopeResolver scopeResolver = context.getBuilderScopeResolver();
+ @Override
+ protected void registerPlannedOutputs(
+ List> tasks, BuilderProcessingContext ctx) {
+ BuilderScopeResolver scopeResolver = ctx.getBuilderScopeResolver();
scopeResolver.resetGeneratedBuilders();
- for (ElementToGenerate elementToGenerate : elementsToGenerate) {
- if (!(elementToGenerate.element() instanceof TypeElement targetType)) {
+ for (CodegenTask task : tasks) {
+ if (!(task.element() instanceof TypeElement targetType)) {
continue;
}
- TypeName beanType = JavaLangMapper.mapToTypeName(targetType, context);
+ TypeName beanType = JavaLangMapper.mapToTypeName(targetType, ctx);
TypeName builderType =
builderTypeName(
targetType,
- effectiveBuilderPackage(
- elementToGenerate.reportingElement(), elementToGenerate.config()),
- elementToGenerate.config());
+ effectiveBuilderPackage(task.reportingElement(), task.configuration()),
+ task.configuration());
scopeResolver.registerGeneratedBuilder(beanType, builderType);
scopeResolver
.resolveGeneratedBuilder(targetType)
.ifPresent(
resolvedBuilder ->
SimpleBuildersSpiIntegration.registerBuilder(
- beanType, resolvedBuilder, elementToGenerate.config().getSetterPrefix()));
- }
- }
-
- /** Generates a builder for each planned element and returns the number of successes. */
- private int generateBuilders(
- List elementsToGenerate, PerformanceTracker tracker) {
- int successfulGenerations = 0;
- for (ElementToGenerate elementToGenerate : elementsToGenerate) {
- Element annotatedElement = elementToGenerate.element();
- context.debugStartOperation("Processing element: " + annotatedElement.getSimpleName());
- tracker.startClass(annotatedElement.getSimpleName().toString());
- try {
- process(annotatedElement, elementToGenerate.config(), builderPackageOf(elementToGenerate));
- successfulGenerations++;
- } catch (BuilderException ex) {
- // By default builder generation failures are warnings so other builders are still
- // generated. In opt-in strict mode they are promoted to errors that fail the build.
- context.reportBasedOnStrictMode(
- elementToGenerate.reportingElement(), MSG_FAILED_TO_GENERATE, ex.getMessage());
- } finally {
- context.debugEndOperation();
- }
+ beanType, resolvedBuilder, task.configuration().getSetterPrefix()));
}
- return successfulGenerations;
}
@Override
- public Set getSupportedOptions() {
- return OPTION_REGISTRY.supportedOptions();
+ protected BuilderDefinitionDto analyse(
+ CodegenTask task, BuilderProcessingContext ctx)
+ throws BuilderException {
+ ctx.initProcessingTarget(new ProcessingTarget<>(task.configuration(), builderPackageOf(task)));
+ PerformanceTracker tracker = ctx.getPerformanceTracker();
+ tracker.startPhase();
+ BuilderDefinitionDto builderDef = extractFromElement(task.element(), ctx);
+ tracker.endPhase(PHASE_BUILDER_DEFINITION_EXTRACTION);
+ return builderDef;
}
@Override
- public SourceVersion getSupportedSourceVersion() {
- return SourceVersion.latestSupported();
- }
-
- /**
- * Fails the build with a descriptive error when the class to be generated already exists (a
- * manually written class or a leftover from a previous compilation).
- *
- * @param classDef the class definition to check
- * @throws CodeGenerationException if a class with the target qualified name already exists
- */
- private void assertTargetClassDoesNotExist(GenerationTargetClassDto classDef)
- throws CodeGenerationException {
- String qualifiedName = classDef.getTypeName().getFullQualifiedName();
- boolean exists;
- try {
- exists = context.getTypeElement(qualifiedName) != null;
- } catch (Exception e) {
- context.debug(
- "Error checking if class '%s' already exists: %s",
- qualifiedName, e.getMessage() != null ? e.getMessage() : "No message");
- exists = false;
- }
- if (exists) {
- throw new CodeGenerationException(
- """
- Builder class '%s' already exists. This may be a manually written builder or a previously generated builder.
- To resolve this:
- 1. If you have a manual builder, consider renaming it or removing @SimpleBuilder from the DTO
- 2. If this is from a previous compilation, clean and rebuild the project
- 3. Check that you're not trying to generate multiple builders for the same DTO
- """
- .formatted(qualifiedName));
- }
- }
-
- private void process(Element annotatedElement, BuilderConfiguration config, String builderPackage)
- throws BuilderException {
- context.initProcessingTarget(new ProcessingTarget<>(config, builderPackage));
- PerformanceTracker tracker = context.getPerformanceTracker();
- // Track Builder Definition Extraction
+ protected List mapToTargets(
+ BuilderDefinitionDto definition,
+ CodegenTask task,
+ BuilderProcessingContext ctx) {
+ PerformanceTracker tracker = ctx.getPerformanceTracker();
tracker.startPhase();
- BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, context);
- tracker.endPhase(PHASE_BUILDER_DEFINITION_EXTRACTION);
+ GenerationTargetClassDto renderingDto =
+ new BuilderToGenerationTypeMapper(task.configuration()).toRenderingDto(definition);
+ tracker.endPhase(PHASE_DTO_MAPPING);
+ return List.of(renderingDto);
+ }
- // Compute class-level metrics now that the definition is available
- int fieldCount = builderDef.getAllFieldsForBuilder().size();
+ @Override
+ protected void afterGenerate(
+ BuilderDefinitionDto definition,
+ List targets,
+ CodegenTask task,
+ BuilderProcessingContext ctx) {
+ GenerationTargetClassDto renderingDto = targets.get(0);
int collectionCount =
(int)
- builderDef.getAllFieldsForBuilder().stream()
+ definition.getAllFieldsForBuilder().stream()
.filter(
f ->
f.getFieldType() instanceof TypeNameList
@@ -646,58 +570,29 @@ private void process(Element annotatedElement, BuilderConfiguration config, Stri
|| f.getFieldType() instanceof TypeNameMap)
.count();
- // Track DTO Mapping
- tracker.startPhase();
- GenerationTargetClassDto renderingDto =
- new BuilderToGenerationTypeMapper(config).toRenderingDto(builderDef);
- tracker.endPhase(PHASE_DTO_MAPPING);
-
- // Track Code Generation (parent phase; sub-phases tracked inside RoasterCodeGenerator)
- tracker.startPhase();
-
- try {
- assertTargetClassDoesNotExist(renderingDto);
- codeGenerator.generateClass(
- renderingDto, formatterResolver.resolve(renderingDto.getFormattingMode()));
- } catch (CodeGenerationException e) {
- throw new BuilderException(annotatedElement, e);
- }
- tracker.endPhase(PHASE_CODE_GENERATION);
-
// Collect info for Jackson Module if enabled
- jacksonModuleGenerator.addEntry(builderDef, annotatedElement);
- context.debug("Jackson module entry added");
+ jacksonModuleGenerator.addEntry(definition, task.element());
+ ctx.debug("Jackson module entry added");
- // End class-level timing (started in caller before config resolution)
- tracker.endClass(fieldCount, collectionCount);
+ // End class-level timing (started by the driver before analysis)
+ ctx.getPerformanceTracker()
+ .endClass(definition.getAllFieldsForBuilder().size(), collectionCount);
// Add summary of what was generated
- context.debugEndOperation(
+ ctx.debugEndOperation(
"Generated builder with %d fields and %d methods for %s",
- builderDef.getAllFieldsForBuilder().size(),
+ definition.getAllFieldsForBuilder().size(),
renderingDto.getMethods().size(),
- builderDef.getBuilderTypeName().getClassName());
+ definition.getBuilderTypeName().getClassName());
}
- /**
- * A type a builder is generated for.
- *
- * @param element the type element to generate the builder for
- * @param config the resolved builder configuration
- * @param reportingElement the element diagnostics are reported on - the {@code @SimpleBuilderFor}
- * holder for external types, otherwise the type itself
- */
- private record ElementToGenerate(
- Element element, BuilderConfiguration config, Element reportingElement) {}
-
/**
* The package the builder is generated into: the configured {@code packageName} option when set,
* otherwise the package of the element the generation is reported on - the
* {@code @SimpleBuilderFor} holder for external types, the annotated type itself otherwise.
*/
- private String builderPackageOf(ElementToGenerate elementToGenerate) {
- return effectiveBuilderPackage(
- elementToGenerate.reportingElement(), elementToGenerate.config());
+ private String builderPackageOf(CodegenTask task) {
+ return effectiveBuilderPackage(task.reportingElement(), task.configuration());
}
/**
@@ -709,16 +604,34 @@ private String effectiveBuilderPackage(Element reportingElement, BuilderConfigur
return configured == null ? context.getPackageName(reportingElement) : configured;
}
- /**
- * Checks whether the provided SourceVersion is at least Java 17 in a backwards compatible way.
- */
- private static boolean isAtLeastJava17(SourceVersion current) {
- try {
- SourceVersion seventeen = SourceVersion.valueOf("RELEASE_17");
- return current.ordinal() >= seventeen.ordinal();
- } catch (IllegalArgumentException ex) {
- // Running on a JDK where RELEASE_17 does not exist (e.g., JDK 8)
- return false;
+ /** Generates all Jackson modules after the last processing round and reports the metrics. */
+ @Override
+ protected void onProcessingOver(BuilderProcessingContext ctx) {
+ SimpleBuildersSpiIntegration.finishCompilation();
+ PerformanceTracker tracker = ctx.getPerformanceTracker();
+ tracker.generateReport(logger);
+
+ List moduleClassDefs = jacksonModuleGenerator.getModuleDefinitions();
+ for (GenerationTargetClassDto moduleClassDef : moduleClassDefs) {
+ String packageName = moduleClassDef.getTypeName().getPackageName();
+ ctx.info("Generating Jackson Module in package '%s'", packageName);
+ try {
+ generateTarget(moduleClassDef);
+ } catch (CodeGenerationException e) {
+ // By default Jackson module generation failures are warnings. In opt-in strict mode
+ // they are promoted to errors that fail the build.
+ ctx.reportBasedOnStrictMode(
+ "simple-builders: Error generating Jackson module for package %s: %s",
+ packageName, e.getMessage());
+ }
}
+ // Reset indentation after Jackson module generation as well
+ ctx.resetIndentation();
+ }
+
+ @Override
+ protected void endRound(BuilderProcessingContext ctx) {
+ // The generating round is done — the registry is final for SPI adapters from here on.
+ SimpleBuildersSpiIntegration.finishCompilation();
}
}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java
index 3f13288a..383df899 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java
@@ -41,6 +41,7 @@
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
+import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.simple.builders.core.enums.OptionState;
import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
@@ -48,7 +49,6 @@
import org.javahelpers.simple.builders.processor.model.type.TypeInstantiation;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext;
-import org.javahelpers.simple.builders.processor.processing.ProcessingTarget;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils;
import org.junit.jupiter.api.BeforeEach;
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java
index 428a3125..5d4b91ef 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java
@@ -28,12 +28,14 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import javax.annotation.processing.ProcessingEnvironment;
+import org.javahelpers.codegen.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.core.enums.AccessModifier;
import org.javahelpers.simple.builders.core.enums.FormattingMode;
import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException;
import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
import org.javahelpers.simple.builders.processor.classgen.SourceFormatter;
import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator;
+import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterSourceFormatter;
import org.javahelpers.simple.builders.processor.classgen.roaster.exceptions.RoasterMapperException;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
@@ -44,7 +46,6 @@
import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext;
-import org.javahelpers.simple.builders.processor.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
import org.javahelpers.simple.builders.processor.testing.CapturingProcessingLogger;
import org.javahelpers.simple.builders.processor.testing.ProcessingEnvironmentStub;
@@ -93,7 +94,8 @@ void shouldWrapRenderingRuntimeExceptionInCodeGenerationException() {
env, logger, context.getPerformanceTracker(), context::reportBasedOnStrictMode));
SourceFormatter formatter =
- new SourceFormatterResolver(env, logger).resolve(FormattingMode.NONE);
+ new SourceFormatterResolver(logger, null, RoasterSourceFormatter::new)
+ .resolve(FormattingMode.NONE);
CodeGenerationException thrown =
assertThrows(
CodeGenerationException.class, () -> generator.generateClass(classDef, formatter));
@@ -129,7 +131,8 @@ void shouldKeepFirstMethodAndWarnOnDuplicateSignature() {
context::reportBasedOnStrictMode));
SourceFormatter formatter =
- new SourceFormatterResolver(env, capturing.logger()).resolve(FormattingMode.NONE);
+ new SourceFormatterResolver(capturing.logger(), null, RoasterSourceFormatter::new)
+ .resolve(FormattingMode.NONE);
// generation itself succeeds; only the file-write step fails on the stub's null filer
assertThrows(NullPointerException.class, () -> generator.generateClass(classDef, formatter));
capturing.assertMessage(
From 80df0d7e9add8facf5f1299b3da4032e18bb7d1d Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 11:53:08 +0000
Subject: [PATCH 007/170] processing: claim annotations via claimsAnnotations
derived from the supported set; additive option seam for wrapper processors
---
.../processing/AbstractCodegenProcessor.java | 46 +++++++++++++++----
1 file changed, 37 insertions(+), 9 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 99b7748b..19c23070 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -24,6 +24,7 @@
package org.javahelpers.codegen.processing;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
@@ -103,21 +104,21 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
}
/**
- * Runs one processing round — see the class javadoc for the spine. Never claims annotations:
- * claiming is all-or-nothing over the supported set and {@link SupportedAnnotationTypes} must
- * stay {@code "*"} to discover user-defined (meta-annotated) annotation types; claiming would
- * hide foreign annotations from later processors (hence the {@code java:S3516} suppression).
+ * Runs one processing round — see the class javadoc for the spine. Claims the supported
+ * annotations only when {@link #claimsAnnotations()} says so — under the default {@code "*"} set
+ * it does not claim, because claiming would hide foreign annotations from later processors (hence
+ * the {@code java:S3516} suppression).
*/
@Override
@SuppressWarnings("java:S3516")
public final boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!supportedJdk) {
// Fail fast: init() already emitted the error; do not attempt any processing.
- return false;
+ return claimsAnnotations();
}
if (roundEnv.processingOver()) {
onProcessingOver(context);
- return false;
+ return claimsAnnotations();
}
PerformanceTracker tracker = context.getPerformanceTracker();
@@ -130,7 +131,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
List> tasks = plan(collected, context);
if (collected.isEmpty() || isFinished(context)) {
- return false;
+ return claimsAnnotations();
}
registerPlannedOutputs(tasks, context);
@@ -172,12 +173,25 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
// Reset indentation level at the end of each processing round to prevent cascading errors
context.resetIndentation();
endRound(context);
- return false;
+ return claimsAnnotations();
}
@Override
public final Set getSupportedOptions() {
- return optionRegistry().supportedOptions();
+ Set options = new LinkedHashSet<>(optionRegistry().supportedOptions());
+ options.addAll(additionalSupportedOptions());
+ return options;
+ }
+
+ /**
+ * Additional supported option names beyond the declared registry — for wrapper or embedder
+ * processors adding their own options without redeclaring the domain's set (the seam MapStruct
+ * uses for its processor wrappers). Default: none.
+ *
+ * @return extra option names
+ */
+ protected Set additionalSupportedOptions() {
+ return Set.of();
}
@Override
@@ -275,6 +289,20 @@ protected abstract SourceFormatterResolver createFormatterResolver(
/** The processor's declared compiler options. */
protected abstract ProcessorOptionRegistry optionRegistry();
+ /**
+ * Whether {@link #process} claims the supported annotations. Default: claims only when the
+ * supported set is concrete — under {@code "*"} (the JDK's default when the domain declares no
+ * {@link SupportedAnnotationTypes} and does not override {@link #getSupportedAnnotationTypes()})
+ * claiming would hide foreign annotations from later processors. Domains narrow the set the
+ * standard JSR-269 way — annotating the processor class or overriding {@code
+ * getSupportedAnnotationTypes()} — and this default claims for them.
+ *
+ * @return {@code true} to claim the supported annotations
+ */
+ protected boolean claimsAnnotations() {
+ return !getSupportedAnnotationTypes().contains("*");
+ }
+
/** The minimum source level the processor supports, as a {@link SourceVersion} name. */
protected abstract String requiredSourceVersion();
From cf8d5868b98b4fe36c0835a376d5d839bc8a3ec9 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 11:54:40 +0000
Subject: [PATCH 008/170] analysis: share array-attribute reading and
class-value resolution via JavaLangAnalyser
---
.../builders/example/PersonDtoMapperImpl.java | 15 +++++---
.../builders/processor/BuilderProcessor.java | 36 +++++++-----------
.../processor/analysis/JavaLangAnalyser.java | 37 +++++++++++++++++++
3 files changed, 59 insertions(+), 29 deletions(-)
diff --git a/example/generated-example-builder/org/javahelpers/simple/builders/example/PersonDtoMapperImpl.java b/example/generated-example-builder/org/javahelpers/simple/builders/example/PersonDtoMapperImpl.java
index 031f0cd7..671699d7 100644
--- a/example/generated-example-builder/org/javahelpers/simple/builders/example/PersonDtoMapperImpl.java
+++ b/example/generated-example-builder/org/javahelpers/simple/builders/example/PersonDtoMapperImpl.java
@@ -15,16 +15,19 @@ public PersonDto copy(PersonDto source) {
return null;
}
- PersonDtoBuilder personDto = PersonDtoBuilder.create();
+ String name = null;
+
+ name = source.getName();
+
+ PersonDto personDto = new PersonDto( name );
- personDto.birthdate( source.getBirthdate() );
- personDto.mannschaft( source.getMannschaft() );
- personDto.name( source.getName() );
List list = source.getNickNames();
if ( list != null ) {
- personDto.nickNames( new ArrayList( list ) );
+ personDto.setNickNames( new ArrayList( list ) );
}
+ personDto.setBirthdate( source.getBirthdate() );
+ personDto.setMannschaft( source.getMannschaft() );
- return personDto.build();
+ return personDto;
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 5584180b..87a1d7cf 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -51,7 +51,6 @@
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
-import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import org.apache.commons.lang3.StringUtils;
import org.javahelpers.codegen.processing.AbstractCodegenProcessor;
@@ -403,7 +402,8 @@ private List extractExternalTargetTypes(
Element holder, AnnotationMirror annotationInstance) throws BuilderException {
Map targets = new LinkedHashMap<>();
// For an array-valued attribute javac always delivers a list, even for a single entry.
- for (Object item : readArrayAttribute(annotationInstance, "value")) {
+ for (Object item :
+ JavaLangAnalyser.findAnnotationAttributeValues(annotationInstance, "value", context)) {
TypeElement target = resolveExternalTargetType(holder, item);
targets.put(target.getQualifiedName().toString(), target);
}
@@ -415,21 +415,14 @@ private List extractExternalTargetTypes(
return List.copyOf(targets.values());
}
- /** Reads an array-typed attribute of an annotation mirror as the raw item list. */
- private List> readArrayAttribute(AnnotationMirror annotationInstance, String attributeName) {
- return JavaLangAnalyser.findAnnotationAttribute(annotationInstance, attributeName, context)
- .map(AnnotationValue::getValue)
- .filter(List.class::isInstance)
- .map(List.class::cast)
- .orElse(List.of());
- }
-
/**
* Reads the {@code sourcePackages} attribute of one {@code @SimpleBuilderFor} annotation
* instance.
*/
private List readSourcePackageNames(AnnotationMirror annotationInstance) {
- return readArrayAttribute(annotationInstance, "sourcePackages").stream()
+ return JavaLangAnalyser.findAnnotationAttributeValues(
+ annotationInstance, "sourcePackages", context)
+ .stream()
.map(item -> item instanceof AnnotationValue value ? value.getValue() : null)
.filter(String.class::isInstance)
.map(String.class::cast)
@@ -479,17 +472,14 @@ private static boolean isConstructibleTopLevelType(TypeElement type) {
/** Resolves one entry of a {@code @SimpleBuilderFor} {@code value} attribute to its type. */
private TypeElement resolveExternalTargetType(Element holder, Object item)
throws BuilderException {
- Object typeValue = item instanceof AnnotationValue value ? value.getValue() : null;
- Element resolved =
- typeValue instanceof TypeMirror typeMirror ? context.asElement(typeMirror) : null;
- if (!(resolved instanceof TypeElement targetType)) {
- throw new BuilderException(
- holder,
- "Value '%s' in @SimpleBuilderFor on '%s' could not be resolved to a type",
- typeValue,
- holder.getSimpleName());
- }
- return targetType;
+ return JavaLangAnalyser.resolveTypeElement(item, context)
+ .orElseThrow(
+ () ->
+ new BuilderException(
+ holder,
+ "Value '%s' in @SimpleBuilderFor on '%s' could not be resolved to a type",
+ item instanceof AnnotationValue value ? value.getValue() : null,
+ holder.getSimpleName()));
}
/** Computes the type name of the builder a given target type would produce. */
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java
index e7ca58bd..102e598e 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java
@@ -312,6 +312,43 @@ public static Optional findAnnotationAttribute(
return Optional.empty();
}
+ /**
+ * Reads an array-typed attribute of an annotation mirror as the raw item list.
+ *
+ * For an array-valued attribute javac always delivers a list, even for a single entry.
+ *
+ * @param annotationMirror the annotation to read
+ * @param attributeName the simple name of the attribute (e.g. {@code "value"})
+ * @param context the processing context providing element utilities
+ * @return the attribute's raw items, or an empty list when absent or not array-valued
+ */
+ public static List> findAnnotationAttributeValues(
+ AnnotationMirror annotationMirror,
+ String attributeName,
+ AnnotationProcessingContext context) {
+ return findAnnotationAttribute(annotationMirror, attributeName, context)
+ .map(AnnotationValue::getValue)
+ .filter(List.class::isInstance)
+ .map(List.class::cast)
+ .orElse(List.of());
+ }
+
+ /**
+ * Resolves one class-valued item of an annotation attribute to its type element.
+ *
+ * @param item a raw attribute item, typically an {@link AnnotationValue} wrapping a {@link
+ * TypeMirror}
+ * @param context the processing context providing type utilities
+ * @return the resolved type element, or empty when the item does not hold a type
+ */
+ public static Optional resolveTypeElement(
+ Object item, AnnotationProcessingContext context) {
+ Object typeValue = item instanceof AnnotationValue value ? value.getValue() : null;
+ Element resolved =
+ typeValue instanceof TypeMirror typeMirror ? context.asElement(typeMirror) : null;
+ return resolved instanceof TypeElement targetType ? Optional.of(targetType) : Optional.empty();
+ }
+
/**
* Single-parameter variant of {@link #findMethodsStatic(TypeElement, List, TypeName,
* AnnotationProcessingContext)}.
From 75574b9aaa601a40e4fdc5d06ea100255886757d Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 12:06:34 +0000
Subject: [PATCH 009/170] processing: declare supported types via
supportedAnnotationTypes() method default, extract findElements hook
---
.../processing/AbstractCodegenProcessor.java | 49 +++++++++++++++----
1 file changed, 39 insertions(+), 10 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 19c23070..b555cff8 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -49,7 +49,7 @@
*
*
* ctx = domain.createContext(env, logger) [in init]
- * elements = elementSpec().collect(annotations, roundEnv, logger)
+ * elements = findElements(annotations, roundEnv, ctx) [default: elementSpec()]
* tasks = domain.plan(elements, ctx)
* domain.registerPlannedOutputs(tasks, ctx) [up-front type recognition]
* for each task (guarded, strict-mode reporting):
@@ -125,7 +125,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
context.info("%s: PROCESSING ROUND START", processorTag());
tracker.startPhase();
- ElementCollection collected = elementSpec().collect(annotations, roundEnv, logger);
+ ElementCollection collected = findElements(annotations, roundEnv, context);
tracker.endPhase(elementSpec().collectionPhase());
context.debug("%s: Processing round started.", processorTag());
@@ -183,6 +183,24 @@ public final Set getSupportedOptions() {
return options;
}
+ @Override
+ public final Set getSupportedAnnotationTypes() {
+ return supportedAnnotationTypes();
+ }
+
+ /**
+ * The annotation type names this processor supports. Default: the {@link
+ * SupportedAnnotationTypes} annotation on the processor class when present, else {@code "*"} —
+ * the framework declares support by method. Domains either annotate their processor class or
+ * override this function (e.g. to compose a computed set).
+ *
+ * @return the supported annotation type names
+ */
+ protected Set supportedAnnotationTypes() {
+ SupportedAnnotationTypes annotation = getClass().getAnnotation(SupportedAnnotationTypes.class);
+ return annotation != null ? Set.copyOf(List.of(annotation.value())) : Set.of("*");
+ }
+
/**
* Additional supported option names beyond the declared registry — for wrapper or embedder
* processors adding their own options without redeclaring the domain's set (the seam MapStruct
@@ -290,17 +308,14 @@ protected abstract SourceFormatterResolver createFormatterResolver(
protected abstract ProcessorOptionRegistry optionRegistry();
/**
- * Whether {@link #process} claims the supported annotations. Default: claims only when the
- * supported set is concrete — under {@code "*"} (the JDK's default when the domain declares no
- * {@link SupportedAnnotationTypes} and does not override {@link #getSupportedAnnotationTypes()})
- * claiming would hide foreign annotations from later processors. Domains narrow the set the
- * standard JSR-269 way — annotating the processor class or overriding {@code
- * getSupportedAnnotationTypes()} — and this default claims for them.
+ * Whether {@link #process} claims the supported annotations. Default {@code false} — claiming
+ * would hide them from later processors, which is especially wrong under the default {@code "*"}
+ * supported set. Override to claim when the processor owns a concrete annotation set.
*
* @return {@code true} to claim the supported annotations
*/
protected boolean claimsAnnotations() {
- return !getSupportedAnnotationTypes().contains("*");
+ return false;
}
/** The minimum source level the processor supports, as a {@link SourceVersion} name. */
@@ -316,11 +331,25 @@ protected boolean claimsAnnotations() {
protected void onInit(
ProcessingEnvironment processingEnv, CompilerArgumentsReader reader, C context) {}
+ /**
+ * Finds the elements of this round the domain generates code for. Default delegates to {@link
+ * #elementSpec()}'s collector; override for custom element discovery.
+ *
+ * @param annotations the annotation types requested this round
+ * @param roundEnv the round environment
+ * @param context the processing context
+ * @return the collected elements
+ */
+ protected ElementCollection findElements(
+ Set extends TypeElement> annotations, RoundEnvironment roundEnv, C context) {
+ return elementSpec().collect(annotations, roundEnv, logger);
+ }
+
/**
* Plans the tasks of this round from the collected elements: resolves per-element configuration,
* expands multi-target declarations and applies domain scoping rules.
*
- * @param collected the elements matched by {@link #elementSpec()}
+ * @param collected the elements matched by {@link #findElements}
* @param context the processing context
* @return the generation plan, possibly empty
*/
From 25021b33e61f1765281673f4ce78a67685e51a4a Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 12:31:31 +0000
Subject: [PATCH 010/170] =?UTF-8?q?processing:=20drop=20redundant=20@Suppo?=
=?UTF-8?q?rtedAnnotationTypes=20=E2=80=94=20framework=20default=20is=20al?=
=?UTF-8?q?ready=20*?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../javahelpers/simple/builders/processor/BuilderProcessor.java | 2 --
1 file changed, 2 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 87a1d7cf..2f93d91e 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -43,7 +43,6 @@
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
-import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
@@ -96,7 +95,6 @@
* driven by {@link AbstractCodegenProcessor}.
*/
@AutoService(Processor.class)
-@SupportedAnnotationTypes("*")
public class BuilderProcessor
extends AbstractCodegenProcessor<
BuilderProcessingContext, BuilderConfiguration, BuilderDefinitionDto> {
From 309fae352aa2dce1e5ac182e242bf31e56b29dd8 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:22:46 +0000
Subject: [PATCH 011/170] =?UTF-8?q?processing:=20common=20CodegenConfigura?=
=?UTF-8?q?tion/CodegenDefinition=20contracts=20=E2=80=94=20strict=20and?=
=?UTF-8?q?=20verbose=20commonly=20supported?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 5 +-
.../processing/CodegenConfiguration.java | 50 +++++++++++++++++++
.../codegen/processing/CodegenDefinition.java | 41 +++++++++++++++
.../codegen/processing/CodegenTask.java | 3 +-
.../model/core/BuilderConfiguration.java | 31 +++++++++++-
.../model/core/BuilderDefinitionDto.java | 3 +-
.../BuilderConfigurationReader.java | 4 ++
7 files changed, 132 insertions(+), 5 deletions(-)
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/CodegenDefinition.java
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index b555cff8..029de33f 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -70,7 +70,10 @@
* @param the domain's configuration type carried by {@link CodegenTask}
* @param the domain's definition type — the analysis result mapped to render targets
*/
-public abstract class AbstractCodegenProcessor
+public abstract class AbstractCodegenProcessor<
+ C extends AnnotationProcessingContext,
+ K extends CodegenConfiguration,
+ D extends CodegenDefinition>
extends AbstractProcessor {
/** The domain's context instance. */
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java
new file mode 100644
index 00000000..8894fa4b
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java
@@ -0,0 +1,50 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+/**
+ * The common configuration contract a code-generation processor's task configuration supports:
+ * strict/fail-fast reporting and verbose diagnostics.
+ *
+ * Framework defaults for new processors: strict enabled, verbose disabled (the javac {@code
+ * -verbose} flag enables verbose output too). Domains may deviate — simple-builders keeps strict
+ * opt-in.
+ */
+public interface CodegenConfiguration {
+
+ /**
+ * Whether generation problems are reported as errors (fail-fast) instead of warnings.
+ *
+ * @return {@code true} for strict reporting
+ */
+ boolean isStrictModeEnabled();
+
+ /**
+ * Whether verbose diagnostic output is enabled.
+ *
+ * @return {@code true} for verbose output
+ */
+ boolean isVerboseEnabled();
+}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenDefinition.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenDefinition.java
new file mode 100644
index 00000000..b7554992
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenDefinition.java
@@ -0,0 +1,41 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import org.javahelpers.simple.builders.processor.model.type.TypeName;
+
+/**
+ * The common contract of a domain's analysed definition — the analysis result the domain maps to
+ * {@code GenerationTargetClassDto} render targets.
+ */
+public interface CodegenDefinition {
+
+ /**
+ * The type this definition was analysed from.
+ *
+ * @return the analysed type's name
+ */
+ TypeName getTypeName();
+}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
index 70e33f62..b2a21028 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
@@ -39,4 +39,5 @@
* @param reportingElement the element diagnostics are reported on — the declaring holder for
* externally declared types, otherwise the element itself
*/
-public record CodegenTask(Element element, K configuration, Element reportingElement) {}
+public record CodegenTask(
+ Element element, K configuration, Element reportingElement) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderConfiguration.java
index 62291f7f..4eaba272 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderConfiguration.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderConfiguration.java
@@ -31,6 +31,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
+import org.javahelpers.codegen.processing.CodegenConfiguration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
import org.javahelpers.simple.builders.core.enums.AccessModifier;
import org.javahelpers.simple.builders.core.enums.FormattingMode;
@@ -83,6 +84,7 @@
* @param formattingMode Formatting mode for generated source code (null = inherit from compiler
* arg)
* @param strict Strict/fail-fast generation mode
+ * @param verbose Verbose diagnostic output
*/
public record BuilderConfiguration(
OptionState generateFieldSupplier,
@@ -120,7 +122,9 @@ public record BuilderConfiguration(
String builderUsageSuffix,
String setterPrefix,
String formattingMode,
- OptionState strict) {
+ OptionState strict,
+ OptionState verbose)
+ implements CodegenConfiguration {
public BuilderConfiguration {
builderGenerationPackages =
@@ -165,6 +169,7 @@ public record BuilderConfiguration(
.setterPrefix("")
.formattingMode(FormattingMode.JDT.getOptionValue())
.strict(DISABLED)
+ .verbose(DISABLED)
.build();
// === Convenience accessors with 'is' prefix for boolean properties ===
@@ -326,10 +331,16 @@ public String getSetterPrefix() {
return setterPrefix;
}
+ @Override
public boolean isStrictModeEnabled() {
return strict == ENABLED;
}
+ @Override
+ public boolean isVerboseEnabled() {
+ return verbose == ENABLED;
+ }
+
public FormattingMode formattingModeEnum() {
return FormattingMode.fromString(formattingMode);
}
@@ -414,6 +425,7 @@ public BuilderConfiguration merge(BuilderConfiguration other) {
.setterPrefix(mergeString(other.setterPrefix, this.setterPrefix))
.formattingMode(mergeString(other.formattingMode, this.formattingMode))
.strict(mergeOptionState(other.strict, this.strict))
+ .verbose(mergeOptionState(other.verbose, this.verbose))
.build();
}
@@ -505,6 +517,7 @@ public String toString() {
.appendIfNotEmpty("setterPrefix", setterPrefix)
.appendIfNotEmpty("formattingMode", formattingMode)
.appendValueIfSet("strict", strict)
+ .appendValueIfSet("verbose", verbose)
.toString();
}
@@ -601,6 +614,9 @@ public static class Builder {
// === Error Handling ===
private OptionState strict = OptionState.UNSET;
+ // === Diagnostics ===
+ private OptionState verbose = OptionState.UNSET;
+
// === Setters ===
public Builder generateSupplier(OptionState value) {
this.generateFieldSupplier = value;
@@ -932,6 +948,16 @@ public Builder strict(boolean value) {
return this;
}
+ public Builder verbose(OptionState value) {
+ this.verbose = value;
+ return this;
+ }
+
+ public Builder verbose(boolean value) {
+ this.verbose = value ? ENABLED : DISABLED;
+ return this;
+ }
+
public BuilderConfiguration build() {
return new BuilderConfiguration(
generateFieldSupplier,
@@ -969,7 +995,8 @@ public BuilderConfiguration build() {
builderUsageSuffix,
setterPrefix,
formattingMode,
- strict);
+ strict,
+ verbose);
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java
index f9c083b8..e7158ad4 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java
@@ -30,6 +30,7 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
+import org.javahelpers.codegen.processing.CodegenDefinition;
import org.javahelpers.simple.builders.core.enums.AccessModifier;
import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto;
import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName;
@@ -53,7 +54,7 @@
* GenerationTargetClassDto} (rendering DTO) during finalization in {@code
* BuilderDefinitionCreator.finalizeDefinition()}.
*/
-public class BuilderDefinitionDto {
+public class BuilderDefinitionDto implements CodegenDefinition {
// --- Builder-specific fields ---
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java
index 2868ffa8..367488fd 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java
@@ -39,6 +39,7 @@
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import org.javahelpers.simple.builders.core.enums.AccessModifier;
+import org.javahelpers.simple.builders.core.enums.OptionState;
import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
@@ -150,6 +151,9 @@ public static BuilderConfiguration readCompilerConfiguration(
Stream.of(CompilerArgumentsEnum.values())
.filter(option -> !option.isAnnotationOnly())
.forEach(option -> option.apply(builder, reader.readValue(option), logger));
+ if (reader.readBooleanValue(CompilerArgumentsEnum.VERBOSE)) {
+ builder.verbose(OptionState.ENABLED);
+ }
return builder.build();
}
From 7292afd94b40d0460e03674eb0179e0d3d225bf4 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:23:03 +0000
Subject: [PATCH 012/170] processing: option registry gains verbose and
formatter-profile contract
---
.../processing/ProcessorOptionRegistry.java | 79 ++++++++++++++++++-
.../builders/processor/BuilderProcessor.java | 6 +-
.../processing/CompilerArgumentsReader.java | 11 +++
3 files changed, 94 insertions(+), 2 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
index 48280d3f..63730fb6 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
@@ -29,6 +29,7 @@
import java.util.List;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
+import org.apache.commons.lang3.StringUtils;
import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader;
import org.javahelpers.simple.builders.processor.processing.CompilerOption;
@@ -41,6 +42,17 @@
public final class ProcessorOptionRegistry {
private final List options;
+ private final CompilerOption verboseOption;
+ private final CompilerOption formatterProfileOption;
+
+ private ProcessorOptionRegistry(
+ Collection extends CompilerOption> options,
+ CompilerOption verboseOption,
+ CompilerOption formatterProfileOption) {
+ this.options = List.copyOf(options);
+ this.verboseOption = verboseOption;
+ this.formatterProfileOption = formatterProfileOption;
+ }
/**
* Creates a registry over the given declared options.
@@ -48,7 +60,17 @@ public final class ProcessorOptionRegistry {
* @param options the processor's compiler options
*/
public ProcessorOptionRegistry(Collection extends CompilerOption> options) {
- this.options = List.copyOf(options);
+ this(options, null, null);
+ }
+
+ /**
+ * A builder for a registry with the framework-level options wired: the verbose flag and the
+ * formatter profile option.
+ *
+ * @return a new builder
+ */
+ public static Builder builder() {
+ return new Builder();
}
/**
@@ -75,4 +97,59 @@ public Set supportedOptions() {
public CompilerArgumentsReader reader(ProcessingEnvironment processingEnv) {
return new CompilerArgumentsReader(processingEnv);
}
+
+ /**
+ * Whether verbose diagnostic output was requested — either via the declared verbose option or the
+ * javac {@code -verbose} flag (also surfaced by {@code mvn -X}).
+ *
+ * @param reader the option value reader
+ * @return {@code true} when verbose output was requested
+ */
+ public boolean isVerboseRequested(CompilerArgumentsReader reader) {
+ return (verboseOption != null && reader.readBooleanValue(verboseOption))
+ || reader.hasOption("-verbose");
+ }
+
+ /**
+ * The configured source-formatter profile name, or {@code null} when no formatter profile option
+ * was declared or set.
+ *
+ * @param reader the option value reader
+ * @return the trimmed profile name, or {@code null}
+ */
+ public String formatterProfile(CompilerArgumentsReader reader) {
+ return formatterProfileOption != null
+ ? StringUtils.trimToNull(reader.readValue(formatterProfileOption))
+ : null;
+ }
+
+ /** Builder for {@link ProcessorOptionRegistry}. */
+ public static final class Builder {
+ private Collection extends CompilerOption> options = List.of();
+ private CompilerOption verboseOption;
+ private CompilerOption formatterProfileOption;
+
+ /** The processor's declared compiler options. */
+ public Builder options(Collection extends CompilerOption> options) {
+ this.options = options;
+ return this;
+ }
+
+ /** The option that enables verbose diagnostic output. */
+ public Builder verboseOption(CompilerOption verboseOption) {
+ this.verboseOption = verboseOption;
+ return this;
+ }
+
+ /** The option that names the source-formatter profile. */
+ public Builder formatterProfileOption(CompilerOption formatterProfileOption) {
+ this.formatterProfileOption = formatterProfileOption;
+ return this;
+ }
+
+ /** Builds the registry. */
+ public ProcessorOptionRegistry build() {
+ return new ProcessorOptionRegistry(options, verboseOption, formatterProfileOption);
+ }
+ }
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 2f93d91e..838b2a22 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -103,7 +103,11 @@ public class BuilderProcessor
/** The processor's declared compiler options — the supported set is derived, not duplicated. */
private static final ProcessorOptionRegistry OPTION_REGISTRY =
- new ProcessorOptionRegistry(List.of(CompilerArgumentsEnum.values()));
+ ProcessorOptionRegistry.builder()
+ .options(List.of(CompilerArgumentsEnum.values()))
+ .verboseOption(CompilerArgumentsEnum.VERBOSE)
+ .formatterProfileOption(CompilerArgumentsEnum.FORMATTER_PROFILE)
+ .build();
/**
* Which elements builders are generated for: types carrying {@code @SimpleBuilder} or a
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java
index f2d0dede..2affe4ce 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java
@@ -99,6 +99,17 @@ public boolean readBooleanValue(CompilerOption argument) {
return readBooleanValue(argument, processingEnv.getOptions(), false);
}
+ /**
+ * Whether an option name is present in the compiler options map — for raw javac flags like {@code
+ * -verbose} that are not declared {@link CompilerOption}s.
+ *
+ * @param optionName the raw option name (e.g. {@code "-verbose"})
+ * @return {@code true} when the option is present
+ */
+ public boolean hasOption(String optionName) {
+ return processingEnv.getOptions().containsKey(optionName);
+ }
+
/**
* Reads the value of a compiler argument as a boolean with an explicit default — the same
* resolution SPI adapters can use on the options map their host framework hands them.
From 3919597b532bbf25262837ba89e34aa2ec92347f Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:23:48 +0000
Subject: [PATCH 013/170] =?UTF-8?q?processing:=20backend=20registry=20reso?=
=?UTF-8?q?lves=20engine=20and=20formatter=20via=20ServiceLoader=20?=
=?UTF-8?q?=E2=80=94=20driver=20defaults=20for=20logger/generator/formatte?=
=?UTF-8?q?r=20creation?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 52 +++++++++++---
.../ClassCodeGeneratorProvider.java | 44 ++++++++++++
.../processing/CodegenBackendRegistry.java | 72 +++++++++++++++++++
.../builders/processor/BuilderProcessor.java | 35 +--------
.../roaster/RoasterCodeGeneratorProvider.java | 38 ++++++++++
.../roaster/RoasterFormatterFactory.java | 39 ++++++++++
...egen.processing.ClassCodeGeneratorProvider | 1 +
...g.SourceFormatterResolver$FormatterFactory | 1 +
8 files changed, 239 insertions(+), 43 deletions(-)
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/ClassCodeGeneratorProvider.java
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGeneratorProvider.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterFormatterFactory.java
create mode 100644 processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.ClassCodeGeneratorProvider
create mode 100644 processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.SourceFormatterResolver$FormatterFactory
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 029de33f..773f7bb9 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -35,6 +35,7 @@
import javax.lang.model.element.TypeElement;
import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException;
+import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
import org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext;
import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader;
@@ -82,6 +83,7 @@ public abstract class AbstractCodegenProcessor<
/** Logger for the domain's hooks (init, last-round reports). */
protected ProcessingLogger logger;
+ private final CodegenBackendRegistry backendRegistry = new CodegenBackendRegistry();
private ClassCodeGenerator codeGenerator;
private SourceFormatterResolver formatterResolver;
private boolean supportedJdk = true;
@@ -281,9 +283,21 @@ private static boolean isAtLeast(SourceVersion current, String requiredName) {
}
}
- /** Creates the logger honoring the domain's verbosity option. */
- protected abstract ProcessingLogger createLogger(
- ProcessingEnvironment processingEnv, CompilerArgumentsReader reader);
+ /**
+ * Creates the logger. Default: verbose when the domain's declared verbose option or the javac
+ * {@code -verbose} flag requests it — see {@link ProcessorOptionRegistry#isVerboseRequested}.
+ */
+ protected ProcessingLogger createLogger(
+ ProcessingEnvironment processingEnv, CompilerArgumentsReader reader) {
+ return optionRegistry().isVerboseRequested(reader)
+ ? ProcessingLogger.createVerbose(processingEnv)
+ : ProcessingLogger.create(processingEnv);
+ }
+
+ /** The backend registry resolving the code-generation engine and formatter factory. */
+ protected CodegenBackendRegistry backendRegistry() {
+ return backendRegistry;
+ }
/**
* Creates the domain's processing context.
@@ -296,13 +310,33 @@ protected abstract ProcessingLogger createLogger(
protected abstract C createContext(
ProcessingEnvironment processingEnv, CompilerArgumentsReader reader, ProcessingLogger logger);
- /** Creates the code generation engine — the {@link ClassCodeGenerator} backend port. */
- protected abstract ClassCodeGenerator createGenerator(
- ProcessingEnvironment processingEnv, ProcessingLogger logger, C context);
+ /**
+ * Creates the code generation engine — the {@link ClassCodeGenerator} backend port. Default: the
+ * engine discovered by {@link CodegenBackendRegistry} over a {@link GenerationEnvironment} wired
+ * from the processing environment and context.
+ */
+ protected ClassCodeGenerator createGenerator(
+ ProcessingEnvironment processingEnv, ProcessingLogger logger, C context) {
+ return backendRegistry.resolveCodeGenerator(
+ new GenerationEnvironment(
+ processingEnv,
+ logger,
+ context.getPerformanceTracker(),
+ context::reportBasedOnStrictMode));
+ }
- /** Creates the source formatter resolver for the configured profile. */
- protected abstract SourceFormatterResolver createFormatterResolver(
- CompilerArgumentsReader reader, ProcessingLogger logger);
+ /**
+ * Creates the source formatter resolver for the configured profile. Default: the profile from
+ * {@link ProcessorOptionRegistry#formatterProfile} and the factory discovered by {@link
+ * CodegenBackendRegistry}.
+ */
+ protected SourceFormatterResolver createFormatterResolver(
+ CompilerArgumentsReader reader, ProcessingLogger logger) {
+ return new SourceFormatterResolver(
+ logger,
+ optionRegistry().formatterProfile(reader),
+ backendRegistry.resolveFormatterFactory());
+ }
/** Which elements the domain generates code for. */
protected abstract AnnotationElementSpec elementSpec();
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ClassCodeGeneratorProvider.java b/processor/src/main/java/org/javahelpers/codegen/processing/ClassCodeGeneratorProvider.java
new file mode 100644
index 00000000..2095ecea
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ClassCodeGeneratorProvider.java
@@ -0,0 +1,44 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
+import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
+
+/**
+ * Service-provider interface for a code-generation backend: providers are discovered via {@link
+ * java.util.ServiceLoader} and create the {@link ClassCodeGenerator} engine the driver generates
+ * through. Register providers in {@code META-INF/services}.
+ */
+public interface ClassCodeGeneratorProvider {
+
+ /**
+ * Creates the code-generation engine for the given generation environment.
+ *
+ * @param environment the generation environment (filer, logger, metrics, issue reporting)
+ * @return the engine instance
+ */
+ ClassCodeGenerator create(GenerationEnvironment environment);
+}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java
new file mode 100644
index 00000000..e24b8fed
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java
@@ -0,0 +1,72 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import java.util.ServiceLoader;
+import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
+import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
+import org.javahelpers.simple.builders.processor.classgen.SourceFormatter;
+
+/**
+ * Resolution point for pluggable code-generation backends: discovers the {@link ClassCodeGenerator}
+ * engine and the {@link SourceFormatter} factory via {@link ServiceLoader}, so a backend library
+ * only has to ship provider entries in {@code META-INF/services}.
+ *
+ * The first provider on the classpath wins; explicit selection and priority ordering are a later
+ * topic.
+ */
+public final class CodegenBackendRegistry {
+
+ /**
+ * Resolves the code-generation engine from the discovered providers.
+ *
+ * @param environment the generation environment handed to the engine
+ * @return the engine instance
+ * @throws IllegalStateException when no provider is on the annotation processor path
+ */
+ public ClassCodeGenerator resolveCodeGenerator(GenerationEnvironment environment) {
+ for (ClassCodeGeneratorProvider provider :
+ ServiceLoader.load(ClassCodeGeneratorProvider.class)) {
+ return provider.create(environment);
+ }
+ throw new IllegalStateException(
+ "No ClassCodeGeneratorProvider found on the annotation processor path");
+ }
+
+ /**
+ * Resolves the source-formatter factory from the discovered providers.
+ *
+ * @return the formatter factory
+ * @throws IllegalStateException when no provider is on the annotation processor path
+ */
+ public SourceFormatterResolver.FormatterFactory resolveFormatterFactory() {
+ for (SourceFormatterResolver.FormatterFactory factory :
+ ServiceLoader.load(SourceFormatterResolver.FormatterFactory.class)) {
+ return factory;
+ }
+ throw new IllegalStateException(
+ "No SourceFormatterResolver.FormatterFactory found on the annotation processor path");
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 838b2a22..7f0564a6 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -51,7 +51,6 @@
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
-import org.apache.commons.lang3.StringUtils;
import org.javahelpers.codegen.processing.AbstractCodegenProcessor;
import org.javahelpers.codegen.processing.AnnotationElementSpec;
import org.javahelpers.codegen.processing.CodegenTask;
@@ -59,7 +58,6 @@
import org.javahelpers.codegen.processing.ElementSelector;
import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.codegen.processing.ProcessorOptionRegistry;
-import org.javahelpers.codegen.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;
@@ -67,11 +65,7 @@
import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver;
import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser;
import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper;
-import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException;
-import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
-import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator;
-import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterSourceFormatter;
import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
@@ -135,17 +129,6 @@ public class BuilderProcessor
private JacksonModuleGenerator jacksonModuleGenerator;
- @Override
- protected ProcessingLogger createLogger(
- ProcessingEnvironment processingEnv, CompilerArgumentsReader reader) {
- ProcessingLogger logger =
- reader.readBooleanValue(CompilerArgumentsEnum.VERBOSE)
- ? ProcessingLogger.createVerbose(processingEnv)
- : ProcessingLogger.create(processingEnv);
- logger.debug("Starting BuilderProcessor...");
- return logger;
- }
-
@Override
protected BuilderProcessingContext createContext(
ProcessingEnvironment processingEnv,
@@ -157,28 +140,12 @@ protected BuilderProcessingContext createContext(
return new BuilderProcessingContext(logger, globalConfig, processingEnv);
}
- @Override
- protected ClassCodeGenerator createGenerator(
- ProcessingEnvironment processingEnv, ProcessingLogger logger, BuilderProcessingContext ctx) {
- return new RoasterCodeGenerator(
- new GenerationEnvironment(
- processingEnv, logger, ctx.getPerformanceTracker(), ctx::reportBasedOnStrictMode));
- }
-
- @Override
- protected SourceFormatterResolver createFormatterResolver(
- CompilerArgumentsReader reader, ProcessingLogger logger) {
- return new SourceFormatterResolver(
- logger,
- StringUtils.trimToNull(reader.readValue(CompilerArgumentsEnum.FORMATTER_PROFILE)),
- RoasterSourceFormatter::new);
- }
-
@Override
protected void onInit(
ProcessingEnvironment processingEnv,
CompilerArgumentsReader reader,
BuilderProcessingContext ctx) {
+ ctx.debug("Starting BuilderProcessor...");
this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig);
// Initialize GeneratorRegistry once during processor initialization
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGeneratorProvider.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGeneratorProvider.java
new file mode 100644
index 00000000..592a438f
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGeneratorProvider.java
@@ -0,0 +1,38 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.processor.classgen.roaster;
+
+import org.javahelpers.codegen.processing.ClassCodeGeneratorProvider;
+import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
+import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
+
+/** {@link ClassCodeGeneratorProvider} for the Roaster backend, discovered via ServiceLoader. */
+public final class RoasterCodeGeneratorProvider implements ClassCodeGeneratorProvider {
+
+ @Override
+ public ClassCodeGenerator create(GenerationEnvironment environment) {
+ return new RoasterCodeGenerator(environment);
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterFormatterFactory.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterFormatterFactory.java
new file mode 100644
index 00000000..7f8aba91
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterFormatterFactory.java
@@ -0,0 +1,39 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.processor.classgen.roaster;
+
+import org.javahelpers.codegen.processing.SourceFormatterResolver;
+import org.javahelpers.simple.builders.core.enums.FormattingMode;
+import org.javahelpers.simple.builders.processor.classgen.SourceFormatter;
+import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
+
+/** Roaster {@link SourceFormatter} factory, discovered via ServiceLoader. */
+public final class RoasterFormatterFactory implements SourceFormatterResolver.FormatterFactory {
+
+ @Override
+ public SourceFormatter create(ProcessingLogger logger, FormattingMode mode, String profile) {
+ return new RoasterSourceFormatter(logger, mode, profile);
+ }
+}
diff --git a/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.ClassCodeGeneratorProvider b/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.ClassCodeGeneratorProvider
new file mode 100644
index 00000000..982b7a50
--- /dev/null
+++ b/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.ClassCodeGeneratorProvider
@@ -0,0 +1 @@
+org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGeneratorProvider
diff --git a/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.SourceFormatterResolver$FormatterFactory b/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.SourceFormatterResolver$FormatterFactory
new file mode 100644
index 00000000..93bc555d
--- /dev/null
+++ b/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.SourceFormatterResolver$FormatterFactory
@@ -0,0 +1 @@
+org.javahelpers.simple.builders.processor.classgen.roaster.RoasterFormatterFactory
From c2620249d964ae9c542866d3cc86dcd4d1801ece Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:24:15 +0000
Subject: [PATCH 014/170] =?UTF-8?q?processing:=20minimum=20abstract=20set?=
=?UTF-8?q?=20=E2=80=94=20SourceVersion-typed=20source=20level,=20defaults?=
=?UTF-8?q?=20for=20tag/kind/phase/options?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 56 ++++++++++---------
.../builders/processor/BuilderProcessor.java | 5 +-
2 files changed, 34 insertions(+), 27 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 773f7bb9..f8e2077f 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -99,9 +99,9 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
onInit(processingEnv, reader, context);
SourceVersion current = processingEnv.getSourceVersion();
- this.supportedJdk = isAtLeast(current, requiredSourceVersion());
+ this.supportedJdk = current.ordinal() >= requiredSourceVersion().ordinal();
if (!this.supportedJdk) {
- String releaseNumber = requiredSourceVersion().replace("RELEASE_", "");
+ String releaseNumber = requiredSourceVersion().toString().replace("RELEASE_", "");
context.error(
"%s requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor.",
processorTag(), releaseNumber, current, releaseNumber);
@@ -270,19 +270,6 @@ private void assertTargetDoesNotExist(GenerationTargetClassDto target)
}
}
- /**
- * Checks whether the current SourceVersion is at least the required one, in a backwards
- * compatible way (the required constant may not exist on older JDKs).
- */
- private static boolean isAtLeast(SourceVersion current, String requiredName) {
- try {
- return current.ordinal() >= SourceVersion.valueOf(requiredName).ordinal();
- } catch (IllegalArgumentException ex) {
- // Running on a JDK where the required release does not exist
- return false;
- }
- }
-
/**
* Creates the logger. Default: verbose when the domain's declared verbose option or the javac
* {@code -verbose} flag requests it — see {@link ProcessorOptionRegistry#isVerboseRequested}.
@@ -341,8 +328,13 @@ protected SourceFormatterResolver createFormatterResolver(
/** Which elements the domain generates code for. */
protected abstract AnnotationElementSpec elementSpec();
- /** The processor's declared compiler options. */
- protected abstract ProcessorOptionRegistry optionRegistry();
+ /**
+ * The processor's declared compiler options. Default: an empty registry — a processor without
+ * options declares nothing.
+ */
+ protected ProcessorOptionRegistry optionRegistry() {
+ return new ProcessorOptionRegistry(List.of());
+ }
/**
* Whether {@link #process} claims the supported annotations. Default {@code false} — claiming
@@ -355,14 +347,26 @@ protected boolean claimsAnnotations() {
return false;
}
- /** The minimum source level the processor supports, as a {@link SourceVersion} name. */
- protected abstract String requiredSourceVersion();
+ /**
+ * The minimum source level the processor supports. Default: {@link
+ * SourceVersion#latestSupported()}.
+ */
+ protected SourceVersion requiredSourceVersion() {
+ return SourceVersion.latestSupported();
+ }
- /** Diagnostic tag prefixing messages, e.g. {@code "simple-builders"}. */
- protected abstract String processorTag();
+ /**
+ * Diagnostic tag prefixing messages, e.g. {@code "simple-builders"}. Default: the processor's
+ * simple class name.
+ */
+ protected String processorTag() {
+ return getClass().getSimpleName();
+ }
- /** What one generated unit is called in diagnostics, e.g. {@code "builder"}. */
- protected abstract String generatedKind();
+ /** What one generated unit is called in diagnostics, e.g. {@code "builder"}. Default: class. */
+ protected String generatedKind() {
+ return "class";
+ }
/** Domain-specific initialization, after the framework wiring is in place. */
protected void onInit(
@@ -449,8 +453,10 @@ protected abstract List mapToTargets(
protected void afterGenerate(
D definition, List targets, CodegenTask task, C context) {}
- /** The performance phase wrapping target generation. */
- protected abstract String generationPhase();
+ /** The performance phase wrapping target generation. Default {@code "code-generation"}. */
+ protected String generationPhase() {
+ return "code-generation";
+ }
/** Callback for the last processing round — deferred outputs and final reports. */
protected void onProcessingOver(C context) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 7f0564a6..4a4708a9 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -43,6 +43,7 @@
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
+import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
@@ -168,8 +169,8 @@ protected ProcessorOptionRegistry optionRegistry() {
}
@Override
- protected String requiredSourceVersion() {
- return "RELEASE_17";
+ protected SourceVersion requiredSourceVersion() {
+ return SourceVersion.RELEASE_17;
}
@Override
From 4582bf7ebc6a0f70f197b705fca7aba6620ad9bf Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:24:43 +0000
Subject: [PATCH 015/170] =?UTF-8?q?processing:=20registerPlannedOutputs=20?=
=?UTF-8?q?generalized=20=E2=80=94=20domain=20supplies=20only=20the=201:1?=
=?UTF-8?q?=20predictedTypeName=20map?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 41 +++++++++++++++++--
.../builders/processor/BuilderProcessor.java | 39 ++++++++++--------
2 files changed, 59 insertions(+), 21 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index f8e2077f..f216d6cc 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -33,10 +33,12 @@
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
+import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper;
import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException;
import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
+import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext;
import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader;
import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker;
@@ -398,13 +400,46 @@ protected ElementCollection findElements(
/**
* Registers the types this round will generate, up front, so references to them resolve during
- * analysis. Domains register into {@link AnnotationProcessingContext#getGeneratedTypes()}.
- * Default: no registration.
+ * analysis. Default: clears {@link AnnotationProcessingContext#getGeneratedTypes()} and registers
+ * each task's {@link #predictedTypeName predicted output} — a 1:1 map from task to generated type
+ * name. Domains may override {@link #registerPlannedOutput} to add per-task registrations (e.g.
+ * SPI), or this whole method for a different shape.
*
* @param tasks the planned tasks
* @param context the processing context
*/
- protected void registerPlannedOutputs(List> tasks, C context) {}
+ protected void registerPlannedOutputs(List> tasks, C context) {
+ context.getGeneratedTypes().clear();
+ for (CodegenTask task : tasks) {
+ registerPlannedOutput(task, context);
+ }
+ }
+
+ /**
+ * Registers one task's predicted output. Default: adds the map {@code element's type →
+ * predictedTypeName} to {@link AnnotationProcessingContext#getGeneratedTypes()}.
+ *
+ * @param task the planned task
+ * @param context the processing context
+ */
+ protected void registerPlannedOutput(CodegenTask task, C context) {
+ TypeName generatedTypeName = predictedTypeName(task, context);
+ if (generatedTypeName != null && task.element() instanceof TypeElement targetType) {
+ context
+ .getGeneratedTypes()
+ .add(JavaLangMapper.mapToTypeName(targetType, context), generatedTypeName);
+ }
+ }
+
+ /**
+ * The name of the type this task will generate — the domain's 1:1 task → output type-name
+ * prediction, evaluated at plan registration time.
+ *
+ * @param task the planned task
+ * @param context the processing context
+ * @return the generated type's name, or {@code null} when the task registers nothing
+ */
+ protected abstract TypeName predictedTypeName(CodegenTask task, C context);
/**
* Whether the domain generates in exactly one round and that round already ran. Default {@code
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 4a4708a9..3ab0015e 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -63,7 +63,6 @@
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFors;
-import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver;
import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser;
import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper;
import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException;
@@ -464,27 +463,31 @@ private TypeName builderTypeName(
* differ from the target's package for {@code @SimpleBuilderFor} targets.
*/
@Override
- protected void registerPlannedOutputs(
- List> tasks, BuilderProcessingContext ctx) {
- BuilderScopeResolver scopeResolver = ctx.getBuilderScopeResolver();
- scopeResolver.resetGeneratedBuilders();
- for (CodegenTask task : tasks) {
- if (!(task.element() instanceof TypeElement targetType)) {
- continue;
- }
- TypeName beanType = JavaLangMapper.mapToTypeName(targetType, ctx);
- TypeName builderType =
- builderTypeName(
- targetType,
- effectiveBuilderPackage(task.reportingElement(), task.configuration()),
- task.configuration());
- scopeResolver.registerGeneratedBuilder(beanType, builderType);
- scopeResolver
+ protected TypeName predictedTypeName(
+ CodegenTask task, BuilderProcessingContext ctx) {
+ if (!(task.element() instanceof TypeElement targetType)) {
+ return null;
+ }
+ return builderTypeName(
+ targetType,
+ effectiveBuilderPackage(task.reportingElement(), task.configuration()),
+ task.configuration());
+ }
+
+ /** SPI-exports the registered builder, on top of the generic generated-type registration. */
+ @Override
+ protected void registerPlannedOutput(
+ CodegenTask task, BuilderProcessingContext ctx) {
+ super.registerPlannedOutput(task, ctx);
+ if (task.element() instanceof TypeElement targetType) {
+ ctx.getBuilderScopeResolver()
.resolveGeneratedBuilder(targetType)
.ifPresent(
resolvedBuilder ->
SimpleBuildersSpiIntegration.registerBuilder(
- beanType, resolvedBuilder, task.configuration().getSetterPrefix()));
+ JavaLangMapper.mapToTypeName(targetType, ctx),
+ resolvedBuilder,
+ task.configuration().getSetterPrefix()));
}
}
From b9c0e2493f4ee93fb5a5ec0d3d867d1275d2e611 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:25:09 +0000
Subject: [PATCH 016/170] =?UTF-8?q?processing:=20deferredTargets=20hook=20?=
=?UTF-8?q?=E2=80=94=20deferred=20outputs=20run=20the=20generic=20last-rou?=
=?UTF-8?q?nd=20loop?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 44 ++++++++++++++++++-
.../builders/processor/BuilderProcessor.java | 30 +++++--------
2 files changed, 54 insertions(+), 20 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index f216d6cc..5bd778a0 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -493,8 +493,48 @@ protected String generationPhase() {
return "code-generation";
}
- /** Callback for the last processing round — deferred outputs and final reports. */
- protected void onProcessingOver(C context) {}
+ /**
+ * Callback for the last processing round — generates the domain's {@link #deferredTargets
+ * deferred targets} through the same guarded path as round targets. Domain extensions wrap this
+ * with their finalization (SPI flush, reports).
+ *
+ * @param context the processing context
+ */
+ protected void onProcessingOver(C context) {
+ for (GenerationTargetClassDto target : deferredTargets(context)) {
+ String packageName = target.getTypeName().getPackageName();
+ String kind = deferredTargetsKind();
+ context.info(
+ "%s: Generating %s in package '%s'",
+ processorTag(), Character.toUpperCase(kind.charAt(0)) + kind.substring(1), packageName);
+ try {
+ generateTarget(target);
+ } catch (Exception e) {
+ context.reportBasedOnStrictMode(
+ "%s: Error generating %s for package %s: %s",
+ processorTag(), kind, packageName, e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * The class definitions generated deferred in the last round — accumulated during {@link
+ * #afterGenerate}. Default: none.
+ *
+ * @param context the processing context
+ * @return the deferred targets, possibly empty
+ */
+ protected List deferredTargets(C context) {
+ return List.of();
+ }
+
+ /**
+ * What the deferred targets are called in diagnostics, e.g. {@code "Jackson module"}. Default:
+ * {@code "deferred target"}.
+ */
+ protected String deferredTargetsKind() {
+ return "deferred target";
+ }
/** Callback at the end of a generating round — domain finalization. */
protected void endRound(C context) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 3ab0015e..aa329450 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -65,7 +65,6 @@
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFors;
import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser;
import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper;
-import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException;
import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
@@ -567,27 +566,22 @@ private String effectiveBuilderPackage(Element reportingElement, BuilderConfigur
return configured == null ? context.getPackageName(reportingElement) : configured;
}
+ @Override
+ protected List deferredTargets(BuilderProcessingContext ctx) {
+ return jacksonModuleGenerator.getModuleDefinitions();
+ }
+
+ @Override
+ protected String deferredTargetsKind() {
+ return "Jackson module";
+ }
+
/** Generates all Jackson modules after the last processing round and reports the metrics. */
@Override
protected void onProcessingOver(BuilderProcessingContext ctx) {
SimpleBuildersSpiIntegration.finishCompilation();
- PerformanceTracker tracker = ctx.getPerformanceTracker();
- tracker.generateReport(logger);
-
- List moduleClassDefs = jacksonModuleGenerator.getModuleDefinitions();
- for (GenerationTargetClassDto moduleClassDef : moduleClassDefs) {
- String packageName = moduleClassDef.getTypeName().getPackageName();
- ctx.info("Generating Jackson Module in package '%s'", packageName);
- try {
- generateTarget(moduleClassDef);
- } catch (CodeGenerationException e) {
- // By default Jackson module generation failures are warnings. In opt-in strict mode
- // they are promoted to errors that fail the build.
- ctx.reportBasedOnStrictMode(
- "simple-builders: Error generating Jackson module for package %s: %s",
- packageName, e.getMessage());
- }
- }
+ ctx.getPerformanceTracker().generateReport(logger);
+ super.onProcessingOver(ctx);
// Reset indentation after Jackson module generation as well
ctx.resetIndentation();
}
From 08d4251f348cb2742934e5be073aaae917b41839 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:25:28 +0000
Subject: [PATCH 017/170] processing: fluent builders for AnnotationElementSpec
and ElementSelector
---
.../processing/AnnotationElementSpec.java | 32 ++++++++++++
.../codegen/processing/ElementSelector.java | 50 ++++++++++++++++++-
.../builders/processor/BuilderProcessor.java | 27 +++++-----
3 files changed, 95 insertions(+), 14 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
index 20f513dd..08650141 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
@@ -63,6 +63,38 @@ public AnnotationElementSpec(List selectors, String collectionP
this.collectionPhase = collectionPhase;
}
+ /**
+ * A builder for an element spec.
+ *
+ * @return a new builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Fluent builder for {@link AnnotationElementSpec}. */
+ public static final class Builder {
+ private final List selectors = new ArrayList<>();
+ private String collectionPhase;
+
+ /** Adds a named selection group, in declaration order. */
+ public Builder selector(ElementSelector selector) {
+ this.selectors.add(selector);
+ return this;
+ }
+
+ /** The performance-tracking phase the collection is reported under, or {@code null}. */
+ public Builder collectionPhase(String collectionPhase) {
+ this.collectionPhase = collectionPhase;
+ return this;
+ }
+
+ /** Builds the spec. */
+ public AnnotationElementSpec build() {
+ return new AnnotationElementSpec(selectors, collectionPhase);
+ }
+ }
+
/**
* The performance-tracking phase the collection is reported under.
*
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
index b76613df..ff439627 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
@@ -44,4 +44,52 @@ public record ElementSelector(
String name,
Set> annotationTypes,
Class extends Annotation> metaMarker,
- Class extends Annotation> optOutAnnotation) {}
+ Class extends Annotation> optOutAnnotation) {
+
+ /**
+ * A builder for an element selector.
+ *
+ * @return a new builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Fluent builder for {@link ElementSelector}. */
+ public static final class Builder {
+ private String name;
+ private Set> annotationTypes = Set.of();
+ private Class extends Annotation> metaMarker;
+ private Class extends Annotation> optOutAnnotation;
+
+ /** The group name results are keyed by. */
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /** Annotation types that directly select an element. */
+ @SafeVarargs
+ public final Builder annotationTypes(Class extends Annotation>... annotationTypes) {
+ this.annotationTypes = Set.of(annotationTypes);
+ return this;
+ }
+
+ /** Meta-annotation marking annotation types that also select, or {@code null}. */
+ public Builder metaMarker(Class extends Annotation> metaMarker) {
+ this.metaMarker = metaMarker;
+ return this;
+ }
+
+ /** Annotation that excludes a selected element again, or {@code null}. */
+ public Builder optOutAnnotation(Class extends Annotation> optOutAnnotation) {
+ this.optOutAnnotation = optOutAnnotation;
+ return this;
+ }
+
+ /** Builds the selector. */
+ public ElementSelector build() {
+ return new ElementSelector(name, annotationTypes, metaMarker, optOutAnnotation);
+ }
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index aa329450..a537b36c 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -109,19 +109,20 @@ public class BuilderProcessor
* {@code @SimpleBuilderFor}.
*/
private static final AnnotationElementSpec ELEMENT_SPEC =
- new AnnotationElementSpec(
- List.of(
- new ElementSelector(
- GROUP_ANNOTATED_ELEMENTS,
- Set.of(),
- Template.class,
- Ignore4BuilderGeneration.class),
- new ElementSelector(
- GROUP_EXTERNAL_TYPE_HOLDERS,
- Set.of(SimpleBuilderFor.class, SimpleBuilderFors.class),
- null,
- null)),
- PHASE_ELEMENT_COLLECTION);
+ AnnotationElementSpec.builder()
+ .selector(
+ ElementSelector.builder()
+ .name(GROUP_ANNOTATED_ELEMENTS)
+ .metaMarker(Template.class)
+ .optOutAnnotation(Ignore4BuilderGeneration.class)
+ .build())
+ .selector(
+ ElementSelector.builder()
+ .name(GROUP_EXTERNAL_TYPE_HOLDERS)
+ .annotationTypes(SimpleBuilderFor.class, SimpleBuilderFors.class)
+ .build())
+ .collectionPhase(PHASE_ELEMENT_COLLECTION)
+ .build();
/** The global configuration resolved from compiler arguments — needed by several hooks. */
private BuilderConfiguration globalConfig;
From 4c2877965636fbebc16c4deaa5ef833b563490c9 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:25:42 +0000
Subject: [PATCH 018/170] processing: javadoc spine marks implement-required
hooks
---
.../processing/AbstractCodegenProcessor.java | 27 +++++++++++--------
1 file changed, 16 insertions(+), 11 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 5bd778a0..c7c6d633 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -48,21 +48,26 @@
* Driver of a generic annotation-processing pipeline: the framework owns the processing lifecycle,
* the domain plugs in its hooks — collect, plan, register, analyse/map, generate, finalize.
*
+ * A new processor implements only the domain hooks marked {@code [implement]}: {@link
+ * #createContext}, {@link #elementSpec}, {@link #plan}, {@link #predictedTypeName}, {@link
+ * #analyse} and {@link #mapToTargets}. Every other step is framework-owned or an optional {@code
+ * [default]} the domain may override.
+ *
*
The round spine:
*
*
- * ctx = domain.createContext(env, logger) [in init]
- * elements = findElements(annotations, roundEnv, ctx) [default: elementSpec()]
- * tasks = domain.plan(elements, ctx)
- * domain.registerPlannedOutputs(tasks, ctx) [up-front type recognition]
- * for each task (guarded, strict-mode reporting):
- * def = domain.analyse(task, ctx)
- * targets = domain.mapToTargets(def, task, ctx)
+ * ctx = createContext(env, reader, logger) [implement — in init]
+ * elements = findElements(annotations, roundEnv, ctx) [default: elementSpec() — implement]
+ * tasks = plan(elements, ctx) [implement]
+ * registerPlannedOutputs(tasks, ctx) [default: predictedTypeName() — implement]
+ * for each task (guarded, strict-mode reporting): [framework]
+ * def = analyse(task, ctx) [implement]
+ * targets = mapToTargets(def, task, ctx) [implement]
* for each target:
- * existsGuard → engine.generate(target, formatterResolver.resolve(target.mode))
- * domain.afterGenerate(def, targets, task, ctx)
- * domain.endRound(ctx)
- * last round: domain.onProcessingOver(ctx) [deferred outputs, reports]
+ * existsGuard → engine.generate(target, formatterResolver.resolve(target.mode)) [framework]
+ * afterGenerate(def, targets, task, ctx) [default: no-op]
+ * endRound(ctx) [default: no-op]
+ * last round: onProcessingOver(ctx) [default: deferredTargets() loop]
*
*
* Per-element failures are caught and reported through {@link
From b6afc43ae38833c7b70d7ba0b86eb67a7eba975d Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:31:06 +0000
Subject: [PATCH 019/170] processing: resolve codegen backends via the
processor-path classloader
---
.../codegen/processing/CodegenBackendRegistry.java | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java
index e24b8fed..67c5eb74 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java
@@ -48,7 +48,8 @@ public final class CodegenBackendRegistry {
*/
public ClassCodeGenerator resolveCodeGenerator(GenerationEnvironment environment) {
for (ClassCodeGeneratorProvider provider :
- ServiceLoader.load(ClassCodeGeneratorProvider.class)) {
+ ServiceLoader.load(
+ ClassCodeGeneratorProvider.class, ClassCodeGeneratorProvider.class.getClassLoader())) {
return provider.create(environment);
}
throw new IllegalStateException(
@@ -63,7 +64,9 @@ public ClassCodeGenerator resolveCodeGenerator(GenerationEnvironment environment
*/
public SourceFormatterResolver.FormatterFactory resolveFormatterFactory() {
for (SourceFormatterResolver.FormatterFactory factory :
- ServiceLoader.load(SourceFormatterResolver.FormatterFactory.class)) {
+ ServiceLoader.load(
+ SourceFormatterResolver.FormatterFactory.class,
+ SourceFormatterResolver.FormatterFactory.class.getClassLoader())) {
return factory;
}
throw new IllegalStateException(
From 5a16341c2da51d806094552af5ba8c9a1c3afe3c Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:31:06 +0000
Subject: [PATCH 020/170] processing: emit processor start marker from base
logger creation
---
.../codegen/processing/AbstractCodegenProcessor.java | 9 ++++++---
.../simple/builders/processor/BuilderProcessor.java | 1 -
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index c7c6d633..6b8d685d 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -283,9 +283,12 @@ private void assertTargetDoesNotExist(GenerationTargetClassDto target)
*/
protected ProcessingLogger createLogger(
ProcessingEnvironment processingEnv, CompilerArgumentsReader reader) {
- return optionRegistry().isVerboseRequested(reader)
- ? ProcessingLogger.createVerbose(processingEnv)
- : ProcessingLogger.create(processingEnv);
+ ProcessingLogger logger =
+ optionRegistry().isVerboseRequested(reader)
+ ? ProcessingLogger.createVerbose(processingEnv)
+ : ProcessingLogger.create(processingEnv);
+ logger.debug("Starting %s...", getClass().getSimpleName());
+ return logger;
}
/** The backend registry resolving the code-generation engine and formatter factory. */
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index a537b36c..f359f884 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -145,7 +145,6 @@ protected void onInit(
ProcessingEnvironment processingEnv,
CompilerArgumentsReader reader,
BuilderProcessingContext ctx) {
- ctx.debug("Starting BuilderProcessor...");
this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig);
// Initialize GeneratorRegistry once during processor initialization
From 5f33384655e259fd9a7616ed717babab4585c4b0 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:31:06 +0000
Subject: [PATCH 021/170] test: pin verbose flag in resolved configuration log
line
---
.../simple/builders/processor/BuilderProcessorTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
index 136590d5..77f97a0e 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
@@ -92,7 +92,7 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() {
compilation,
// Processor init
"[DEBUG] Starting BuilderProcessor...",
- "[DEBUG] Loaded global configuration from compiler arguments: BuilderConfiguration[]",
+ "[DEBUG] Loaded global configuration from compiler arguments: BuilderConfiguration[verbose=ENABLED]",
"[DEBUG] Initializing generator registry",
"[DEBUG] ├─ Loaded 15 method generators and 10 builder enhancers total",
"[DEBUG] └─ Initialized GeneratorRegistry with 15 method generators and 10 builder",
From 584e6b32499276e9e0772cc9c99984f2c6a0689b Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:31:06 +0000
Subject: [PATCH 022/170] example: regenerate PersonDtoMapperImpl with
builder-based mapping
---
.../builders/example/PersonDtoMapperImpl.java | 15 ++++++---------
1 file changed, 6 insertions(+), 9 deletions(-)
diff --git a/example/generated-example-builder/org/javahelpers/simple/builders/example/PersonDtoMapperImpl.java b/example/generated-example-builder/org/javahelpers/simple/builders/example/PersonDtoMapperImpl.java
index 671699d7..031f0cd7 100644
--- a/example/generated-example-builder/org/javahelpers/simple/builders/example/PersonDtoMapperImpl.java
+++ b/example/generated-example-builder/org/javahelpers/simple/builders/example/PersonDtoMapperImpl.java
@@ -15,19 +15,16 @@ public PersonDto copy(PersonDto source) {
return null;
}
- String name = null;
-
- name = source.getName();
-
- PersonDto personDto = new PersonDto( name );
+ PersonDtoBuilder personDto = PersonDtoBuilder.create();
+ personDto.birthdate( source.getBirthdate() );
+ personDto.mannschaft( source.getMannschaft() );
+ personDto.name( source.getName() );
List list = source.getNickNames();
if ( list != null ) {
- personDto.setNickNames( new ArrayList( list ) );
+ personDto.nickNames( new ArrayList( list ) );
}
- personDto.setBirthdate( source.getBirthdate() );
- personDto.setMannschaft( source.getMannschaft() );
- return personDto;
+ return personDto.build();
}
}
From 6b27c6f7975af4c4a25a4aaa581db23f1a8c3a35 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:32:08 +0000
Subject: [PATCH 023/170] =?UTF-8?q?processing:=20drop=20unreachable=20-ver?=
=?UTF-8?q?bose=20detection=20=E2=80=94=20javac=20exposes=20only=20-A=20op?=
=?UTF-8?q?tions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/AbstractCodegenProcessor.java | 4 ++--
.../codegen/processing/ProcessorOptionRegistry.java | 9 +++++----
.../processor/processing/CompilerArgumentsReader.java | 11 -----------
3 files changed, 7 insertions(+), 17 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 6b8d685d..d71c6559 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -278,8 +278,8 @@ private void assertTargetDoesNotExist(GenerationTargetClassDto target)
}
/**
- * Creates the logger. Default: verbose when the domain's declared verbose option or the javac
- * {@code -verbose} flag requests it — see {@link ProcessorOptionRegistry#isVerboseRequested}.
+ * Creates the logger. Default: verbose when the domain's declared verbose option requests it —
+ * see {@link ProcessorOptionRegistry#isVerboseRequested}.
*/
protected ProcessingLogger createLogger(
ProcessingEnvironment processingEnv, CompilerArgumentsReader reader) {
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
index 63730fb6..a1681470 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
@@ -99,15 +99,16 @@ public CompilerArgumentsReader reader(ProcessingEnvironment processingEnv) {
}
/**
- * Whether verbose diagnostic output was requested — either via the declared verbose option or the
- * javac {@code -verbose} flag (also surfaced by {@code mvn -X}).
+ * Whether verbose diagnostic output was requested via the declared verbose option. The javac
+ * {@code -verbose} flag (and {@code mvn -X} which sets it) cannot be detected here — javac
+ * exposes only {@code -A} options in {@link
+ * javax.annotation.processing.ProcessingEnvironment#getOptions()}.
*
* @param reader the option value reader
* @return {@code true} when verbose output was requested
*/
public boolean isVerboseRequested(CompilerArgumentsReader reader) {
- return (verboseOption != null && reader.readBooleanValue(verboseOption))
- || reader.hasOption("-verbose");
+ return verboseOption != null && reader.readBooleanValue(verboseOption);
}
/**
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java
index 2affe4ce..f2d0dede 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java
@@ -99,17 +99,6 @@ public boolean readBooleanValue(CompilerOption argument) {
return readBooleanValue(argument, processingEnv.getOptions(), false);
}
- /**
- * Whether an option name is present in the compiler options map — for raw javac flags like {@code
- * -verbose} that are not declared {@link CompilerOption}s.
- *
- * @param optionName the raw option name (e.g. {@code "-verbose"})
- * @return {@code true} when the option is present
- */
- public boolean hasOption(String optionName) {
- return processingEnv.getOptions().containsKey(optionName);
- }
-
/**
* Reads the value of a compiler argument as a boolean with an explicit default — the same
* resolution SPI adapters can use on the options map their host framework hands them.
From 0e7688c7b453d687840747308ae97b411320ac96 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:48:49 +0000
Subject: [PATCH 024/170] processing: static processor declarations as
CodegenProcessorConfiguration record passed to the base constructor
---
.../processing/AbstractCodegenProcessor.java | 76 ++++----
.../CodegenProcessorConfiguration.java | 177 ++++++++++++++++++
.../builders/processor/BuilderProcessor.java | 53 ++----
3 files changed, 231 insertions(+), 75 deletions(-)
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index d71c6559..a25280be 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -48,16 +48,17 @@
* Driver of a generic annotation-processing pipeline: the framework owns the processing lifecycle,
* the domain plugs in its hooks — collect, plan, register, analyse/map, generate, finalize.
*
- * A new processor implements only the domain hooks marked {@code [implement]}: {@link
- * #createContext}, {@link #elementSpec}, {@link #plan}, {@link #predictedTypeName}, {@link
- * #analyse} and {@link #mapToTargets}. Every other step is framework-owned or an optional {@code
- * [default]} the domain may override.
+ *
A new processor declares its static surface once as {@link CodegenProcessorConfiguration}
+ * (passed to the constructor — element spec, options, source level, diagnostic names) and
+ * implements only the domain hooks marked {@code [implement]}: {@link #createContext}, {@link
+ * #plan}, {@link #predictedTypeName}, {@link #analyse} and {@link #mapToTargets}. Every other step
+ * is framework-owned or an optional {@code [default]} the domain may override.
*
*
The round spine:
*
*
* ctx = createContext(env, reader, logger) [implement — in init]
- * elements = findElements(annotations, roundEnv, ctx) [default: elementSpec() — implement]
+ * elements = findElements(annotations, roundEnv, ctx) [default: configuration's elementSpec]
* tasks = plan(elements, ctx) [implement]
* registerPlannedOutputs(tasks, ctx) [default: predictedTypeName() — implement]
* for each task (guarded, strict-mode reporting): [framework]
@@ -90,11 +91,22 @@ public abstract class AbstractCodegenProcessor<
/** Logger for the domain's hooks (init, last-round reports). */
protected ProcessingLogger logger;
+ private final CodegenProcessorConfiguration processorConfiguration;
private final CodegenBackendRegistry backendRegistry = new CodegenBackendRegistry();
private ClassCodeGenerator codeGenerator;
private SourceFormatterResolver formatterResolver;
private boolean supportedJdk = true;
+ /**
+ * Creates the processor on its static configuration. javac still instantiates the concrete
+ * subclass through its no-arg constructor, which hands the configuration to this constructor.
+ *
+ * @param processorConfiguration the processor's static declarations
+ */
+ protected AbstractCodegenProcessor(CodegenProcessorConfiguration processorConfiguration) {
+ this.processorConfiguration = processorConfiguration;
+ }
+
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
@@ -145,7 +157,6 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
if (collected.isEmpty() || isFinished(context)) {
return claimsAnnotations();
}
-
registerPlannedOutputs(tasks, context);
int successfulGenerations = 0;
@@ -335,15 +346,12 @@ protected SourceFormatterResolver createFormatterResolver(
backendRegistry.resolveFormatterFactory());
}
- /** Which elements the domain generates code for. */
- protected abstract AnnotationElementSpec elementSpec();
+ private AnnotationElementSpec elementSpec() {
+ return processorConfiguration.elementSpec();
+ }
- /**
- * The processor's declared compiler options. Default: an empty registry — a processor without
- * options declares nothing.
- */
- protected ProcessorOptionRegistry optionRegistry() {
- return new ProcessorOptionRegistry(List.of());
+ private ProcessorOptionRegistry optionRegistry() {
+ return processorConfiguration.optionRegistry();
}
/**
@@ -357,25 +365,17 @@ protected boolean claimsAnnotations() {
return false;
}
- /**
- * The minimum source level the processor supports. Default: {@link
- * SourceVersion#latestSupported()}.
- */
- protected SourceVersion requiredSourceVersion() {
- return SourceVersion.latestSupported();
+ private SourceVersion requiredSourceVersion() {
+ return processorConfiguration.requiredSourceVersion();
}
- /**
- * Diagnostic tag prefixing messages, e.g. {@code "simple-builders"}. Default: the processor's
- * simple class name.
- */
- protected String processorTag() {
- return getClass().getSimpleName();
+ private String processorTag() {
+ String configured = processorConfiguration.processorTag();
+ return configured != null ? configured : getClass().getSimpleName();
}
- /** What one generated unit is called in diagnostics, e.g. {@code "builder"}. Default: class. */
- protected String generatedKind() {
- return "class";
+ private String generatedKind() {
+ return processorConfiguration.generatedKind();
}
/** Domain-specific initialization, after the framework wiring is in place. */
@@ -383,8 +383,9 @@ protected void onInit(
ProcessingEnvironment processingEnv, CompilerArgumentsReader reader, C context) {}
/**
- * Finds the elements of this round the domain generates code for. Default delegates to {@link
- * #elementSpec()}'s collector; override for custom element discovery.
+ * Finds the elements of this round the domain generates code for. Default delegates to the
+ * configured {@link CodegenProcessorConfiguration#elementSpec}'s collector; override for custom
+ * element discovery.
*
* @param annotations the annotation types requested this round
* @param roundEnv the round environment
@@ -496,9 +497,8 @@ protected abstract List mapToTargets(
protected void afterGenerate(
D definition, List targets, CodegenTask task, C context) {}
- /** The performance phase wrapping target generation. Default {@code "code-generation"}. */
- protected String generationPhase() {
- return "code-generation";
+ private String generationPhase() {
+ return processorConfiguration.generationPhase();
}
/**
@@ -536,12 +536,8 @@ protected List deferredTargets(C context) {
return List.of();
}
- /**
- * What the deferred targets are called in diagnostics, e.g. {@code "Jackson module"}. Default:
- * {@code "deferred target"}.
- */
- protected String deferredTargetsKind() {
- return "deferred target";
+ private String deferredTargetsKind() {
+ return processorConfiguration.deferredTargetsKind();
}
/** Callback at the end of a generating round — domain finalization. */
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
new file mode 100644
index 00000000..bf6848ef
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
@@ -0,0 +1,177 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import java.util.List;
+import javax.lang.model.SourceVersion;
+
+/**
+ * Static declarations of an {@link AbstractCodegenProcessor}: everything the framework needs to
+ * know up front, collected in one record the processor hands to the base constructor.
+ *
+ * Only {@link #elementSpec} is required; all other components carry defaults a minimal processor
+ * can leave unset. Use {@link #builder()} to build an instance.
+ *
+ * @param elementSpec which elements the domain generates code for (required)
+ * @param optionRegistry the processor's declared compiler options; default: an empty registry
+ * @param requiredSourceVersion the minimum source level; default: {@link
+ * SourceVersion#latestSupported()}
+ * @param processorTag diagnostic tag prefixing messages; default: {@code null} — the processor's
+ * simple class name is used
+ * @param generatedKind what one generated unit is called in diagnostics; default: {@code "class"}
+ * @param generationPhase the performance phase wrapping target generation; default: {@code
+ * "code-generation"}
+ * @param deferredTargetsKind what deferred targets are called in diagnostics; default: {@code
+ * "deferred target"}
+ */
+public record CodegenProcessorConfiguration(
+ AnnotationElementSpec elementSpec,
+ ProcessorOptionRegistry optionRegistry,
+ SourceVersion requiredSourceVersion,
+ String processorTag,
+ String generatedKind,
+ String generationPhase,
+ String deferredTargetsKind) {
+
+ /**
+ * Creates a builder with all defaults applied — only {@link #elementSpec(AnnotationElementSpec)}
+ * must be set.
+ *
+ * @return a new builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Fluent builder for {@link CodegenProcessorConfiguration}. */
+ public static final class Builder {
+ private AnnotationElementSpec elementSpec;
+ private ProcessorOptionRegistry optionRegistry = new ProcessorOptionRegistry(List.of());
+ private SourceVersion requiredSourceVersion = SourceVersion.latestSupported();
+ private String processorTag;
+ private String generatedKind = "class";
+ private String generationPhase = "code-generation";
+ private String deferredTargetsKind = "deferred target";
+
+ private Builder() {}
+
+ /**
+ * Sets which elements the domain generates code for (required).
+ *
+ * @param elementSpec the element specification
+ * @return this builder
+ */
+ public Builder elementSpec(AnnotationElementSpec elementSpec) {
+ this.elementSpec = elementSpec;
+ return this;
+ }
+
+ /**
+ * Sets the processor's declared compiler options.
+ *
+ * @param optionRegistry the option registry
+ * @return this builder
+ */
+ public Builder optionRegistry(ProcessorOptionRegistry optionRegistry) {
+ this.optionRegistry = optionRegistry;
+ return this;
+ }
+
+ /**
+ * Sets the minimum source level the processor supports.
+ *
+ * @param requiredSourceVersion the required source version
+ * @return this builder
+ */
+ public Builder requiredSourceVersion(SourceVersion requiredSourceVersion) {
+ this.requiredSourceVersion = requiredSourceVersion;
+ return this;
+ }
+
+ /**
+ * Sets the diagnostic tag prefixing messages.
+ *
+ * @param processorTag the processor tag
+ * @return this builder
+ */
+ public Builder processorTag(String processorTag) {
+ this.processorTag = processorTag;
+ return this;
+ }
+
+ /**
+ * Sets what one generated unit is called in diagnostics.
+ *
+ * @param generatedKind the generated kind
+ * @return this builder
+ */
+ public Builder generatedKind(String generatedKind) {
+ this.generatedKind = generatedKind;
+ return this;
+ }
+
+ /**
+ * Sets the performance phase wrapping target generation.
+ *
+ * @param generationPhase the generation phase name
+ * @return this builder
+ */
+ public Builder generationPhase(String generationPhase) {
+ this.generationPhase = generationPhase;
+ return this;
+ }
+
+ /**
+ * Sets what deferred targets are called in diagnostics.
+ *
+ * @param deferredTargetsKind the deferred targets kind
+ * @return this builder
+ */
+ public Builder deferredTargetsKind(String deferredTargetsKind) {
+ this.deferredTargetsKind = deferredTargetsKind;
+ return this;
+ }
+
+ /**
+ * Builds the configuration.
+ *
+ * @return the configuration
+ * @throws IllegalStateException when no {@link #elementSpec} was set
+ */
+ public CodegenProcessorConfiguration build() {
+ if (elementSpec == null) {
+ throw new IllegalStateException("elementSpec is required");
+ }
+ return new CodegenProcessorConfiguration(
+ elementSpec,
+ optionRegistry,
+ requiredSourceVersion,
+ processorTag,
+ generatedKind,
+ generationPhase,
+ deferredTargetsKind);
+ }
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index f359f884..415fba38 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -54,6 +54,7 @@
import javax.lang.model.util.ElementFilter;
import org.javahelpers.codegen.processing.AbstractCodegenProcessor;
import org.javahelpers.codegen.processing.AnnotationElementSpec;
+import org.javahelpers.codegen.processing.CodegenProcessorConfiguration;
import org.javahelpers.codegen.processing.CodegenTask;
import org.javahelpers.codegen.processing.ElementCollection;
import org.javahelpers.codegen.processing.ElementSelector;
@@ -124,6 +125,23 @@ public class BuilderProcessor
.collectionPhase(PHASE_ELEMENT_COLLECTION)
.build();
+ /** The processor's static declarations handed to the framework driver. */
+ private static final CodegenProcessorConfiguration PROCESSOR_CONFIGURATION =
+ CodegenProcessorConfiguration.builder()
+ .elementSpec(ELEMENT_SPEC)
+ .optionRegistry(OPTION_REGISTRY)
+ .requiredSourceVersion(SourceVersion.RELEASE_17)
+ .processorTag("simple-builders")
+ .generatedKind("builder")
+ .generationPhase(PHASE_CODE_GENERATION)
+ .deferredTargetsKind("Jackson module")
+ .build();
+
+ /** Creates the processor — javac instantiates it reflectively. */
+ public BuilderProcessor() {
+ super(PROCESSOR_CONFIGURATION);
+ }
+
/** The global configuration resolved from compiler arguments — needed by several hooks. */
private BuilderConfiguration globalConfig;
@@ -156,36 +174,6 @@ protected void onInit(
}
}
- @Override
- protected AnnotationElementSpec elementSpec() {
- return ELEMENT_SPEC;
- }
-
- @Override
- protected ProcessorOptionRegistry optionRegistry() {
- return OPTION_REGISTRY;
- }
-
- @Override
- protected SourceVersion requiredSourceVersion() {
- return SourceVersion.RELEASE_17;
- }
-
- @Override
- protected String processorTag() {
- return "simple-builders";
- }
-
- @Override
- protected String generatedKind() {
- return "builder";
- }
-
- @Override
- protected String generationPhase() {
- return PHASE_CODE_GENERATION;
- }
-
@Override
protected List> plan(
ElementCollection collected, BuilderProcessingContext ctx) {
@@ -571,11 +559,6 @@ protected List deferredTargets(BuilderProcessingContex
return jacksonModuleGenerator.getModuleDefinitions();
}
- @Override
- protected String deferredTargetsKind() {
- return "Jackson module";
- }
-
/** Generates all Jackson modules after the last processing round and reports the metrics. */
@Override
protected void onProcessingOver(BuilderProcessingContext ctx) {
From afb610543c672b990967cbb131f6506e888c162c Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 14:48:58 +0000
Subject: [PATCH 025/170] =?UTF-8?q?processing:=20isFinished=20defaults=20t?=
=?UTF-8?q?o=20one-shot=20=E2=80=94=20first=20planned=20round=20marks=20th?=
=?UTF-8?q?e=20processor=20finished?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index a25280be..f94010fd 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -96,6 +96,7 @@ public abstract class AbstractCodegenProcessor<
private ClassCodeGenerator codeGenerator;
private SourceFormatterResolver formatterResolver;
private boolean supportedJdk = true;
+ private boolean processedOnce;
/**
* Creates the processor on its static configuration. javac still instantiates the concrete
@@ -157,6 +158,12 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
if (collected.isEmpty() || isFinished(context)) {
return claimsAnnotations();
}
+ if (!tasks.isEmpty()) {
+ // The default one-shot is consumed by the first round that planned work — an empty round
+ // leaves it open for a later one carrying matching elements.
+ processedOnce = true;
+ }
+
registerPlannedOutputs(tasks, context);
int successfulGenerations = 0;
@@ -451,14 +458,16 @@ protected void registerPlannedOutput(CodegenTask task, C context) {
protected abstract TypeName predictedTypeName(CodegenTask task, C context);
/**
- * Whether the domain generates in exactly one round and that round already ran. Default {@code
- * false} — every round with matching elements is processed.
+ * Whether the domain's generating round already ran. Default: one-shot — the base marks the
+ * processor finished after the first round that planned tasks, so elements first appearing in a
+ * later round get nothing. Override for a custom lifecycle rule (e.g. a cross-compilation
+ * finished-marker).
*
* @param context the processing context
* @return {@code true} to skip this round
*/
protected boolean isFinished(C context) {
- return false;
+ return processedOnce;
}
/**
From 8c2e101e10755781ebf2c8955fc65fb9c014a353 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 16:34:03 +0000
Subject: [PATCH 026/170] processing: rename predictedTypeName to
predictedGeneratedTypeName
---
.../processing/AbstractCodegenProcessor.java | 18 +++++++++---------
.../builders/processor/BuilderProcessor.java | 2 +-
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index f94010fd..60d6e5c6 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -51,8 +51,8 @@
* A new processor declares its static surface once as {@link CodegenProcessorConfiguration}
* (passed to the constructor — element spec, options, source level, diagnostic names) and
* implements only the domain hooks marked {@code [implement]}: {@link #createContext}, {@link
- * #plan}, {@link #predictedTypeName}, {@link #analyse} and {@link #mapToTargets}. Every other step
- * is framework-owned or an optional {@code [default]} the domain may override.
+ * #plan}, {@link #predictedGeneratedTypeName}, {@link #analyse} and {@link #mapToTargets}. Every
+ * other step is framework-owned or an optional {@code [default]} the domain may override.
*
*
The round spine:
*
@@ -60,7 +60,7 @@
* ctx = createContext(env, reader, logger) [implement — in init]
* elements = findElements(annotations, roundEnv, ctx) [default: configuration's elementSpec]
* tasks = plan(elements, ctx) [implement]
- * registerPlannedOutputs(tasks, ctx) [default: predictedTypeName() — implement]
+ * registerPlannedOutputs(tasks, ctx) [default: predictedGeneratedTypeName() — implement]
* for each task (guarded, strict-mode reporting): [framework]
* def = analyse(task, ctx) [implement]
* targets = mapToTargets(def, task, ctx) [implement]
@@ -417,9 +417,9 @@ protected ElementCollection findElements(
/**
* Registers the types this round will generate, up front, so references to them resolve during
* analysis. Default: clears {@link AnnotationProcessingContext#getGeneratedTypes()} and registers
- * each task's {@link #predictedTypeName predicted output} — a 1:1 map from task to generated type
- * name. Domains may override {@link #registerPlannedOutput} to add per-task registrations (e.g.
- * SPI), or this whole method for a different shape.
+ * each task's {@link #predictedGeneratedTypeName predicted output} — a 1:1 map from task to
+ * generated type name. Domains may override {@link #registerPlannedOutput} to add per-task
+ * registrations (e.g. SPI), or this whole method for a different shape.
*
* @param tasks the planned tasks
* @param context the processing context
@@ -433,13 +433,13 @@ protected void registerPlannedOutputs(List> tasks, C context) {
/**
* Registers one task's predicted output. Default: adds the map {@code element's type →
- * predictedTypeName} to {@link AnnotationProcessingContext#getGeneratedTypes()}.
+ * predictedGeneratedTypeName} to {@link AnnotationProcessingContext#getGeneratedTypes()}.
*
* @param task the planned task
* @param context the processing context
*/
protected void registerPlannedOutput(CodegenTask task, C context) {
- TypeName generatedTypeName = predictedTypeName(task, context);
+ TypeName generatedTypeName = predictedGeneratedTypeName(task, context);
if (generatedTypeName != null && task.element() instanceof TypeElement targetType) {
context
.getGeneratedTypes()
@@ -455,7 +455,7 @@ protected void registerPlannedOutput(CodegenTask task, C context) {
* @param context the processing context
* @return the generated type's name, or {@code null} when the task registers nothing
*/
- protected abstract TypeName predictedTypeName(CodegenTask task, C context);
+ protected abstract TypeName predictedGeneratedTypeName(CodegenTask task, C context);
/**
* Whether the domain's generating round already ran. Default: one-shot — the base marks the
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 415fba38..30f1c083 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -450,7 +450,7 @@ private TypeName builderTypeName(
* differ from the target's package for {@code @SimpleBuilderFor} targets.
*/
@Override
- protected TypeName predictedTypeName(
+ protected TypeName predictedGeneratedTypeName(
CodegenTask task, BuilderProcessingContext ctx) {
if (!(task.element() instanceof TypeElement targetType)) {
return null;
From 3a2fd4d01f1e9b0d52555288eba2310a85c70bab Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 16:35:19 +0000
Subject: [PATCH 027/170] =?UTF-8?q?processing:=20SPI=20registration=20deco?=
=?UTF-8?q?rates=20the=20predicted=20name=20directly=20=E2=80=94=20resolve?=
=?UTF-8?q?GeneratedBuilder=20becomes=20dead?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../builders/processor/BuilderProcessor.java | 15 ++++----
.../analysis/BuilderScopeResolver.java | 34 ++++++-------------
2 files changed, 16 insertions(+), 33 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 30f1c083..354e87a1 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -466,15 +466,12 @@ protected TypeName predictedGeneratedTypeName(
protected void registerPlannedOutput(
CodegenTask task, BuilderProcessingContext ctx) {
super.registerPlannedOutput(task, ctx);
- if (task.element() instanceof TypeElement targetType) {
- ctx.getBuilderScopeResolver()
- .resolveGeneratedBuilder(targetType)
- .ifPresent(
- resolvedBuilder ->
- SimpleBuildersSpiIntegration.registerBuilder(
- JavaLangMapper.mapToTypeName(targetType, ctx),
- resolvedBuilder,
- task.configuration().getSetterPrefix()));
+ TypeName builderName = predictedGeneratedTypeName(task, ctx);
+ if (task.element() instanceof TypeElement targetType && builderName != null) {
+ SimpleBuildersSpiIntegration.registerBuilder(
+ JavaLangMapper.mapToTypeName(targetType, ctx),
+ ctx.getBuilderScopeResolver().generatedBuilderContract(builderName),
+ task.configuration().getSetterPrefix());
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
index 34222b1d..42057c83 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
@@ -143,26 +143,17 @@ public Optional resolveUsableBuilderType(TypeElement referenced
}
/**
- * The builder contract of a type whose builder this processor registered for the current round —
- * always the generated {@code create()} factory for the empty path and the constructor for the
- * copy path. Only the SPI integrations consume this, to publish the emitted contract.
+ * Wraps a builder type name with the instantiation contract every generated builder emits — a
+ * static {@code create()} factory for the empty path and a constructor for the copy path.
*
- * Unlike {@link #resolveUsableBuilderType(TypeElement)} this does not read the per-element
- * configuration and is safe to call during generation-plan registration.
- *
- * @param referencedType the type element being referenced
- * @return the resolved builder, or empty if no generated builder is registered for the type
+ * @param builder the generated builder's type name
+ * @return the resolved builder carrying its contract
*/
- public Optional resolveGeneratedBuilder(TypeElement referencedType) {
- TypeName referencedTypeName = JavaLangMapper.mapToTypeName(referencedType, context);
- return generatedBuilders
- .findGenerated(referencedTypeName)
- .map(
- builder ->
- new ResolvedBuilder(
- builder,
- new TypeInstantiation.StaticFactoryCall(GENERATED_FACTORY_METHOD_NAME),
- new TypeInstantiation.ConstructorCall()));
+ public ResolvedBuilder generatedBuilderContract(TypeName builder) {
+ return new ResolvedBuilder(
+ builder,
+ new TypeInstantiation.StaticFactoryCall(GENERATED_FACTORY_METHOD_NAME),
+ new TypeInstantiation.ConstructorCall());
}
/**
@@ -247,12 +238,7 @@ private Optional resolve(TypeElement referencedType) {
if (generatedBuilder.isPresent()) {
// Our generators always emit a static create() and no create(T) - the empty path uses
// the factory, the copy path the constructor
- return generatedBuilder.map(
- builder ->
- new ResolvedBuilder(
- builder,
- new TypeInstantiation.StaticFactoryCall(GENERATED_FACTORY_METHOD_NAME),
- new TypeInstantiation.ConstructorCall()));
+ return generatedBuilder.map(this::generatedBuilderContract);
}
// Reusing builders not generated in this round - nested or anchored inside the referenced
From fc7ca0630206db147bb6778bad9ac0ffa668dbd2 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:48:19 +0000
Subject: [PATCH 028/170] =?UTF-8?q?processing:=20selector=20declaration=20?=
=?UTF-8?q?via=20consumer=20=E2=80=94=20less=20builder=20boilerplate=20in?=
=?UTF-8?q?=20the=20spec?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AnnotationElementSpec.java | 8 ++++++++
.../builders/processor/BuilderProcessor.java | 19 +++++++++----------
2 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
index 08650141..4da944fa 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
@@ -31,6 +31,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Consumer;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
@@ -83,6 +84,13 @@ public Builder selector(ElementSelector selector) {
return this;
}
+ /** Adds a selection group declared inline on a fresh {@link ElementSelector.Builder}. */
+ public Builder selector(Consumer selectorDeclaration) {
+ ElementSelector.Builder selectorBuilder = ElementSelector.builder();
+ selectorDeclaration.accept(selectorBuilder);
+ return selector(selectorBuilder.build());
+ }
+
/** The performance-tracking phase the collection is reported under, or {@code null}. */
public Builder collectionPhase(String collectionPhase) {
this.collectionPhase = collectionPhase;
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 354e87a1..f37a1a5c 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -57,7 +57,6 @@
import org.javahelpers.codegen.processing.CodegenProcessorConfiguration;
import org.javahelpers.codegen.processing.CodegenTask;
import org.javahelpers.codegen.processing.ElementCollection;
-import org.javahelpers.codegen.processing.ElementSelector;
import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.codegen.processing.ProcessorOptionRegistry;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
@@ -112,16 +111,16 @@ public class BuilderProcessor
private static final AnnotationElementSpec ELEMENT_SPEC =
AnnotationElementSpec.builder()
.selector(
- ElementSelector.builder()
- .name(GROUP_ANNOTATED_ELEMENTS)
- .metaMarker(Template.class)
- .optOutAnnotation(Ignore4BuilderGeneration.class)
- .build())
+ selector ->
+ selector
+ .name(GROUP_ANNOTATED_ELEMENTS)
+ .metaMarker(Template.class)
+ .optOutAnnotation(Ignore4BuilderGeneration.class))
.selector(
- ElementSelector.builder()
- .name(GROUP_EXTERNAL_TYPE_HOLDERS)
- .annotationTypes(SimpleBuilderFor.class, SimpleBuilderFors.class)
- .build())
+ selector ->
+ selector
+ .name(GROUP_EXTERNAL_TYPE_HOLDERS)
+ .annotationTypes(SimpleBuilderFor.class, SimpleBuilderFors.class))
.collectionPhase(PHASE_ELEMENT_COLLECTION)
.build();
From 4fa976454774a4fbd5f4310f173a26249268bcf5 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:48:29 +0000
Subject: [PATCH 029/170] =?UTF-8?q?processing:=20fold=20onInit=20into=20cr?=
=?UTF-8?q?eateContext=20=E2=80=94=20one=20init=20hook=20fewer,=20no=20pos?=
=?UTF-8?q?t-wiring=20need?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/AbstractCodegenProcessor.java | 5 -----
.../simple/builders/processor/BuilderProcessor.java | 11 +++--------
2 files changed, 3 insertions(+), 13 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 60d6e5c6..4b661d2d 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -116,7 +116,6 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
this.context = createContext(processingEnv, reader, logger);
this.codeGenerator = createGenerator(processingEnv, logger, context);
this.formatterResolver = createFormatterResolver(reader, logger);
- onInit(processingEnv, reader, context);
SourceVersion current = processingEnv.getSourceVersion();
this.supportedJdk = current.ordinal() >= requiredSourceVersion().ordinal();
@@ -385,10 +384,6 @@ private String generatedKind() {
return processorConfiguration.generatedKind();
}
- /** Domain-specific initialization, after the framework wiring is in place. */
- protected void onInit(
- ProcessingEnvironment processingEnv, CompilerArgumentsReader reader, C context) {}
-
/**
* Finds the elements of this round the domain generates code for. Default delegates to the
* configured {@link CodegenProcessorConfiguration#elementSpec}'s collector; override for custom
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index f37a1a5c..86771569 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -154,15 +154,9 @@ protected BuilderProcessingContext createContext(
this.globalConfig = BuilderConfigurationReader.readCompilerConfiguration(reader, logger);
logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig);
SimpleBuildersSpiIntegration.initCompilation(processingEnv.getElementUtils());
- return new BuilderProcessingContext(logger, globalConfig, processingEnv);
- }
-
- @Override
- protected void onInit(
- ProcessingEnvironment processingEnv,
- CompilerArgumentsReader reader,
- BuilderProcessingContext ctx) {
this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig);
+ BuilderProcessingContext ctx =
+ new BuilderProcessingContext(logger, globalConfig, processingEnv);
// Initialize GeneratorRegistry once during processor initialization
ctx.debugStartOperation("Initializing generator registry");
@@ -171,6 +165,7 @@ protected void onInit(
} finally {
ctx.debugEndOperation();
}
+ return ctx;
}
@Override
From eb0c1721b8d3ac658cb7d91e07423ff6d636098c Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:48:43 +0000
Subject: [PATCH 030/170] processing: claimsAnnotations takes the supported
set; fail-fast returns literal false
---
.../processing/AbstractCodegenProcessor.java | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 4b661d2d..95962d23 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -129,7 +129,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
/**
* Runs one processing round — see the class javadoc for the spine. Claims the supported
- * annotations only when {@link #claimsAnnotations()} says so — under the default {@code "*"} set
+ * annotations only when {@link #claimsAnnotations(Set)} says so — under the default {@code "*"} set
* it does not claim, because claiming would hide foreign annotations from later processors (hence
* the {@code java:S3516} suppression).
*/
@@ -138,11 +138,11 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
public final boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!supportedJdk) {
// Fail fast: init() already emitted the error; do not attempt any processing.
- return claimsAnnotations();
+ return false;
}
if (roundEnv.processingOver()) {
onProcessingOver(context);
- return claimsAnnotations();
+ return claimsAnnotations(supportedAnnotationTypes());
}
PerformanceTracker tracker = context.getPerformanceTracker();
@@ -155,7 +155,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
List> tasks = plan(collected, context);
if (collected.isEmpty() || isFinished(context)) {
- return claimsAnnotations();
+ return claimsAnnotations(supportedAnnotationTypes());
}
if (!tasks.isEmpty()) {
// The default one-shot is consumed by the first round that planned work — an empty round
@@ -202,7 +202,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
// Reset indentation level at the end of each processing round to prevent cascading errors
context.resetIndentation();
endRound(context);
- return claimsAnnotations();
+ return claimsAnnotations(supportedAnnotationTypes());
}
@Override
@@ -365,9 +365,10 @@ private ProcessorOptionRegistry optionRegistry() {
* would hide them from later processors, which is especially wrong under the default {@code "*"}
* supported set. Override to claim when the processor owns a concrete annotation set.
*
+ * @param supportedTypes the supported annotation type names the round was processed for
* @return {@code true} to claim the supported annotations
*/
- protected boolean claimsAnnotations() {
+ protected boolean claimsAnnotations(Set supportedTypes) {
return false;
}
From 0a4b1512a9cc3a8ffda904cf1da3318e9188e91d Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:48:59 +0000
Subject: [PATCH 031/170] =?UTF-8?q?processing:=20supported=20types=20deriv?=
=?UTF-8?q?e=20from=20the=20element=20spec=20=E2=80=94=20'*'=20only=20when?=
=?UTF-8?q?=20meta-marker=20discovery=20needs=20it?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 35 +++++++++++++++----
.../processing/AnnotationElementSpec.java | 26 ++++++++++++++
2 files changed, 54 insertions(+), 7 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 95962d23..6f0ed9e3 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -26,6 +26,7 @@
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
@@ -129,9 +130,9 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
/**
* Runs one processing round — see the class javadoc for the spine. Claims the supported
- * annotations only when {@link #claimsAnnotations(Set)} says so — under the default {@code "*"} set
- * it does not claim, because claiming would hide foreign annotations from later processors (hence
- * the {@code java:S3516} suppression).
+ * annotations only when {@link #claimsAnnotations(Set)} says so — under the default {@code "*"}
+ * set it does not claim, because claiming would hide foreign annotations from later processors
+ * (hence the {@code java:S3516} suppression).
*/
@Override
@SuppressWarnings("java:S3516")
@@ -219,15 +220,35 @@ public final Set getSupportedAnnotationTypes() {
/**
* The annotation type names this processor supports. Default: the {@link
- * SupportedAnnotationTypes} annotation on the processor class when present, else {@code "*"} —
- * the framework declares support by method. Domains either annotate their processor class or
- * override this function (e.g. to compose a computed set).
+ * SupportedAnnotationTypes} annotation on the processor class when present, else the concrete
+ * annotation types of the configured {@link CodegenProcessorConfiguration#elementSpec} — or
+ * {@code "*"} when a selector uses meta-annotation discovery (its types cannot be enumerated).
+ * Domains either annotate their processor class, declare concrete types in the element spec or
+ * override this function.
*
* @return the supported annotation type names
*/
protected Set supportedAnnotationTypes() {
+ return supportedAnnotationTypesByAnnotation().orElseGet(this::supportedAnnotationTypesBySpec);
+ }
+
+ /**
+ * The supported annotation type names declared by the {@link SupportedAnnotationTypes} annotation
+ * on the processor class.
+ *
+ * @return the declared names, or empty when the class is not annotated
+ */
+ protected Optional> supportedAnnotationTypesByAnnotation() {
SupportedAnnotationTypes annotation = getClass().getAnnotation(SupportedAnnotationTypes.class);
- return annotation != null ? Set.copyOf(List.of(annotation.value())) : Set.of("*");
+ return annotation != null
+ ? Optional.of(Set.copyOf(List.of(annotation.value())))
+ : Optional.empty();
+ }
+
+ private Set supportedAnnotationTypesBySpec() {
+ return elementSpec().hasMetaMarkedSelector()
+ ? Set.of("*")
+ : elementSpec().annotationTypeNames();
}
/**
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
index 4da944fa..56c7f66e 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
@@ -28,6 +28,7 @@
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -112,6 +113,31 @@ public String collectionPhase() {
return collectionPhase;
}
+ /**
+ * Whether any selector uses meta-annotation discovery. Then the supported annotation types cannot
+ * be enumerated — user-defined annotation types match the marker without being named.
+ *
+ * @return {@code true} when a selector has a meta marker
+ */
+ public boolean hasMetaMarkedSelector() {
+ return selectors.stream().anyMatch(selector -> selector.metaMarker() != null);
+ }
+
+ /**
+ * The concrete annotation type names declared across all selectors.
+ *
+ * @return the canonical annotation type names the spec selects directly
+ */
+ public Set annotationTypeNames() {
+ Set names = new LinkedHashSet<>();
+ for (ElementSelector selector : selectors) {
+ for (Class extends java.lang.annotation.Annotation> type : selector.annotationTypes()) {
+ names.add(type.getCanonicalName());
+ }
+ }
+ return names;
+ }
+
/**
* Collects the elements selected by every selector in the given round.
*
From 1e6878a03ebf7897926039475b53523e0350abc6 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:49:21 +0000
Subject: [PATCH 032/170] processing: collection/generation phases are
framework constants, not domain declarations
---
.../processing/AbstractCodegenProcessor.java | 14 ++++++-----
.../processing/AnnotationElementSpec.java | 24 ++-----------------
.../CodegenProcessorConfiguration.java | 16 -------------
.../builders/processor/BuilderProcessor.java | 4 ----
4 files changed, 10 insertions(+), 48 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 6f0ed9e3..6867a549 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -86,6 +86,12 @@ public abstract class AbstractCodegenProcessor<
D extends CodegenDefinition>
extends AbstractProcessor {
+ /** Performance phase the element collection is reported under — framework-owned. */
+ private static final String PHASE_ELEMENT_COLLECTION = "Element Collection";
+
+ /** Performance phase target generation is reported under — framework-owned. */
+ private static final String PHASE_CODE_GENERATION = "Code Generation";
+
/** The domain's context instance. */
protected C context;
@@ -151,7 +157,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
tracker.startPhase();
ElementCollection collected = findElements(annotations, roundEnv, context);
- tracker.endPhase(elementSpec().collectionPhase());
+ tracker.endPhase(PHASE_ELEMENT_COLLECTION);
context.debug("%s: Processing round started.", processorTag());
List> tasks = plan(collected, context);
@@ -177,7 +183,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
for (GenerationTargetClassDto target : targets) {
generateTarget(target);
}
- tracker.endPhase(generationPhase());
+ tracker.endPhase(PHASE_CODE_GENERATION);
afterGenerate(definition, targets, task, context);
successfulGenerations++;
} catch (Exception ex) {
@@ -523,10 +529,6 @@ protected abstract List mapToTargets(
protected void afterGenerate(
D definition, List targets, CodegenTask task, C context) {}
- private String generationPhase() {
- return processorConfiguration.generationPhase();
- }
-
/**
* Callback for the last processing round — generates the domain's {@link #deferredTargets
* deferred targets} through the same guarded path as round targets. Domain extensions wrap this
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
index 56c7f66e..0913465b 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
@@ -51,18 +51,14 @@
public final class AnnotationElementSpec {
private final List selectors;
- private final String collectionPhase;
/**
* Creates a spec.
*
* @param selectors the named selection groups, in declaration order
- * @param collectionPhase performance-tracking phase name the collection is reported under, or
- * {@code null} to skip timing
*/
- public AnnotationElementSpec(List selectors, String collectionPhase) {
+ public AnnotationElementSpec(List selectors) {
this.selectors = selectors;
- this.collectionPhase = collectionPhase;
}
/**
@@ -77,7 +73,6 @@ public static Builder builder() {
/** Fluent builder for {@link AnnotationElementSpec}. */
public static final class Builder {
private final List selectors = new ArrayList<>();
- private String collectionPhase;
/** Adds a named selection group, in declaration order. */
public Builder selector(ElementSelector selector) {
@@ -92,27 +87,12 @@ public Builder selector(Consumer selectorDeclaration) {
return selector(selectorBuilder.build());
}
- /** The performance-tracking phase the collection is reported under, or {@code null}. */
- public Builder collectionPhase(String collectionPhase) {
- this.collectionPhase = collectionPhase;
- return this;
- }
-
/** Builds the spec. */
public AnnotationElementSpec build() {
- return new AnnotationElementSpec(selectors, collectionPhase);
+ return new AnnotationElementSpec(selectors);
}
}
- /**
- * The performance-tracking phase the collection is reported under.
- *
- * @return the phase name, or {@code null} when untimed
- */
- public String collectionPhase() {
- return collectionPhase;
- }
-
/**
* Whether any selector uses meta-annotation discovery. Then the supported annotation types cannot
* be enumerated — user-defined annotation types match the marker without being named.
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
index bf6848ef..f0235752 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
@@ -41,8 +41,6 @@
* @param processorTag diagnostic tag prefixing messages; default: {@code null} — the processor's
* simple class name is used
* @param generatedKind what one generated unit is called in diagnostics; default: {@code "class"}
- * @param generationPhase the performance phase wrapping target generation; default: {@code
- * "code-generation"}
* @param deferredTargetsKind what deferred targets are called in diagnostics; default: {@code
* "deferred target"}
*/
@@ -52,7 +50,6 @@ public record CodegenProcessorConfiguration(
SourceVersion requiredSourceVersion,
String processorTag,
String generatedKind,
- String generationPhase,
String deferredTargetsKind) {
/**
@@ -72,7 +69,6 @@ public static final class Builder {
private SourceVersion requiredSourceVersion = SourceVersion.latestSupported();
private String processorTag;
private String generatedKind = "class";
- private String generationPhase = "code-generation";
private String deferredTargetsKind = "deferred target";
private Builder() {}
@@ -132,17 +128,6 @@ public Builder generatedKind(String generatedKind) {
return this;
}
- /**
- * Sets the performance phase wrapping target generation.
- *
- * @param generationPhase the generation phase name
- * @return this builder
- */
- public Builder generationPhase(String generationPhase) {
- this.generationPhase = generationPhase;
- return this;
- }
-
/**
* Sets what deferred targets are called in diagnostics.
*
@@ -170,7 +155,6 @@ public CodegenProcessorConfiguration build() {
requiredSourceVersion,
processorTag,
generatedKind,
- generationPhase,
deferredTargetsKind);
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 86771569..8b6f989c 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -26,10 +26,8 @@
import static org.javahelpers.simple.builders.processor.processing.BuilderDefinitionCreator.extractFromElement;
import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_BUILDER_DEFINITION_EXTRACTION;
-import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_CODE_GENERATION;
import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_CONFIGURATION_RESOLUTION;
import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_DTO_MAPPING;
-import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_ELEMENT_COLLECTION;
import com.google.auto.service.AutoService;
import java.util.ArrayList;
@@ -121,7 +119,6 @@ public class BuilderProcessor
selector
.name(GROUP_EXTERNAL_TYPE_HOLDERS)
.annotationTypes(SimpleBuilderFor.class, SimpleBuilderFors.class))
- .collectionPhase(PHASE_ELEMENT_COLLECTION)
.build();
/** The processor's static declarations handed to the framework driver. */
@@ -132,7 +129,6 @@ public class BuilderProcessor
.requiredSourceVersion(SourceVersion.RELEASE_17)
.processorTag("simple-builders")
.generatedKind("builder")
- .generationPhase(PHASE_CODE_GENERATION)
.deferredTargetsKind("Jackson module")
.build();
From 7d97195dc119747950a0844115494fcd1e5ac55b Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:49:41 +0000
Subject: [PATCH 033/170] processing: registration predicts a name list; SPI
publish moves to onPlannedOutputRegistered
---
.../processing/AbstractCodegenProcessor.java | 42 ++++++++++---------
.../builders/processor/BuilderProcessor.java | 33 ++++++++-------
2 files changed, 40 insertions(+), 35 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 6867a549..6d2f7e4d 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -61,7 +61,7 @@
* ctx = createContext(env, reader, logger) [implement — in init]
* elements = findElements(annotations, roundEnv, ctx) [default: configuration's elementSpec]
* tasks = plan(elements, ctx) [implement]
- * registerPlannedOutputs(tasks, ctx) [default: predictedGeneratedTypeName() — implement]
+ * registerPlannedOutputs(tasks, ctx) [default: registerPlannedGenerationTargets() — implement]
* for each task (guarded, strict-mode reporting): [framework]
* def = analyse(task, ctx) [implement]
* targets = mapToTargets(def, task, ctx) [implement]
@@ -440,9 +440,9 @@ protected ElementCollection findElements(
/**
* Registers the types this round will generate, up front, so references to them resolve during
* analysis. Default: clears {@link AnnotationProcessingContext#getGeneratedTypes()} and registers
- * each task's {@link #predictedGeneratedTypeName predicted output} — a 1:1 map from task to
- * generated type name. Domains may override {@link #registerPlannedOutput} to add per-task
- * registrations (e.g. SPI), or this whole method for a different shape.
+ * every name each task's {@link #registerPlannedGenerationTargets} predicts — a task may produce
+ * several generated types. Each registration fires {@link #onPlannedOutputRegistered} for domain
+ * extras (e.g. SPI publication); override this whole method only for a different shape.
*
* @param tasks the planned tasks
* @param context the processing context
@@ -450,35 +450,39 @@ protected ElementCollection findElements(
protected void registerPlannedOutputs(List> tasks, C context) {
context.getGeneratedTypes().clear();
for (CodegenTask task : tasks) {
- registerPlannedOutput(task, context);
+ if (!(task.element() instanceof TypeElement targetType)) {
+ continue;
+ }
+ TypeName elementName = JavaLangMapper.mapToTypeName(targetType, context);
+ for (TypeName generated : registerPlannedGenerationTargets(task, context)) {
+ context.getGeneratedTypes().add(elementName, generated);
+ onPlannedOutputRegistered(task, generated, context);
+ }
}
}
/**
- * Registers one task's predicted output. Default: adds the map {@code element's type →
- * predictedGeneratedTypeName} to {@link AnnotationProcessingContext#getGeneratedTypes()}.
+ * The names of the types this task will generate — the domain's task → output type-name
+ * predictions, evaluated at registration time. Keep this pure: registrations and their side
+ * effects belong to {@link #onPlannedOutputRegistered}.
*
* @param task the planned task
* @param context the processing context
+ * @return the generated type names (empty when the task registers nothing)
*/
- protected void registerPlannedOutput(CodegenTask task, C context) {
- TypeName generatedTypeName = predictedGeneratedTypeName(task, context);
- if (generatedTypeName != null && task.element() instanceof TypeElement targetType) {
- context
- .getGeneratedTypes()
- .add(JavaLangMapper.mapToTypeName(targetType, context), generatedTypeName);
- }
- }
+ protected abstract List registerPlannedGenerationTargets(
+ CodegenTask task, C context);
/**
- * The name of the type this task will generate — the domain's 1:1 task → output type-name
- * prediction, evaluated at plan registration time.
+ * Runs after a predicted type name is registered for a task — the extension point for domain
+ * extras on registration (e.g. SPI publication). Default: nothing.
*
* @param task the planned task
+ * @param generatedName the type name just registered for it
* @param context the processing context
- * @return the generated type's name, or {@code null} when the task registers nothing
*/
- protected abstract TypeName predictedGeneratedTypeName(CodegenTask task, C context);
+ protected void onPlannedOutputRegistered(
+ CodegenTask task, TypeName generatedName, C context) {}
/**
* Whether the domain's generating round already ran. Default: one-shot — the base marks the
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 8b6f989c..40c3368a 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -435,32 +435,33 @@ private TypeName builderTypeName(
}
/**
- * Registers the types whose builders will be generated this round with the scope resolver, so it
- * can trust them without a type search. The actual builder type name is registered, which may
- * differ from the target's package for {@code @SimpleBuilderFor} targets.
+ * The builder type this task will generate — registered up front so references to it resolve
+ * during analysis. The package may differ from the target's for {@code @SimpleBuilderFor}
+ * targets.
*/
@Override
- protected TypeName predictedGeneratedTypeName(
+ protected List registerPlannedGenerationTargets(
CodegenTask task, BuilderProcessingContext ctx) {
if (!(task.element() instanceof TypeElement targetType)) {
- return null;
+ return List.of();
}
- return builderTypeName(
- targetType,
- effectiveBuilderPackage(task.reportingElement(), task.configuration()),
- task.configuration());
+ return List.of(
+ builderTypeName(
+ targetType,
+ effectiveBuilderPackage(task.reportingElement(), task.configuration()),
+ task.configuration()));
}
- /** SPI-exports the registered builder, on top of the generic generated-type registration. */
+ /** SPI-exports the registered builder on top of the generic generated-type registration. */
@Override
- protected void registerPlannedOutput(
- CodegenTask task, BuilderProcessingContext ctx) {
- super.registerPlannedOutput(task, ctx);
- TypeName builderName = predictedGeneratedTypeName(task, ctx);
- if (task.element() instanceof TypeElement targetType && builderName != null) {
+ protected void onPlannedOutputRegistered(
+ CodegenTask task,
+ TypeName generatedName,
+ BuilderProcessingContext ctx) {
+ if (task.element() instanceof TypeElement targetType) {
SimpleBuildersSpiIntegration.registerBuilder(
JavaLangMapper.mapToTypeName(targetType, ctx),
- ctx.getBuilderScopeResolver().generatedBuilderContract(builderName),
+ ctx.getBuilderScopeResolver().generatedBuilderContract(generatedName),
task.configuration().getSetterPrefix());
}
}
From 4578d76d18a5579f78d32d7522372e4a28638b34 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:50:14 +0000
Subject: [PATCH 034/170] processing: task carries its output package; the
driver initializes ProcessingTarget before analyse
---
.../processing/AbstractCodegenProcessor.java | 2 ++
.../codegen/processing/CodegenTask.java | 5 +++-
.../builders/processor/BuilderProcessor.java | 27 +++++--------------
.../AnnotationProcessingContext.java | 26 ++++++++++++++++++
.../processing/BuilderProcessingContext.java | 17 ++----------
5 files changed, 40 insertions(+), 37 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 6d2f7e4d..71538024 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -177,6 +177,8 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
context.debugStartOperation("Processing element: " + task.element().getSimpleName());
tracker.startClass(task.element().getSimpleName().toString());
try {
+ context.initProcessingTarget(
+ new ProcessingTarget<>(task.configuration(), task.outputPackage()));
D definition = analyse(task, context);
List targets = mapToTargets(definition, task, context);
tracker.startPhase();
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
index b2a21028..a1f70cf1 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
@@ -38,6 +38,9 @@
* @param configuration the resolved per-element configuration
* @param reportingElement the element diagnostics are reported on — the declaring holder for
* externally declared types, otherwise the element itself
+ * @param outputPackage the package generated code is written to, or {@code null} when the domain
+ * does not carry one — planning computes it so the driver can initialize {@link
+ * ProcessingTarget} before analysis
*/
public record CodegenTask(
- Element element, K configuration, Element reportingElement) {}
+ Element element, K configuration, Element reportingElement, String outputPackage) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 40c3368a..e67aeaef 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -55,7 +55,6 @@
import org.javahelpers.codegen.processing.CodegenProcessorConfiguration;
import org.javahelpers.codegen.processing.CodegenTask;
import org.javahelpers.codegen.processing.ElementCollection;
-import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.codegen.processing.ProcessorOptionRegistry;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
@@ -255,10 +254,10 @@ private Optional> planGenerationOfAnnotatedEle
if (!context.getBuilderScopeResolver().isInGenerationScope(annotatedElement, config)) {
return Optional.empty();
}
- alreadyPlannedBuilders.add(
- builderTypeName(
- annotatedElement, effectiveBuilderPackage(annotatedElement, config), config));
- return Optional.of(new CodegenTask<>(annotatedElement, config, annotatedElement));
+ String builderPackage = effectiveBuilderPackage(annotatedElement, config);
+ alreadyPlannedBuilders.add(builderTypeName(annotatedElement, builderPackage, config));
+ return Optional.of(
+ new CodegenTask<>(annotatedElement, config, annotatedElement, builderPackage));
}
/**
@@ -335,7 +334,7 @@ private Optional> planExternalTarget(
builderName.getFullQualifiedName());
return Optional.empty();
}
- return Optional.of(new CodegenTask<>(target, config, holder));
+ return Optional.of(new CodegenTask<>(target, config, holder, builderPackage));
}
/**
@@ -445,11 +444,7 @@ protected List registerPlannedGenerationTargets(
if (!(task.element() instanceof TypeElement targetType)) {
return List.of();
}
- return List.of(
- builderTypeName(
- targetType,
- effectiveBuilderPackage(task.reportingElement(), task.configuration()),
- task.configuration()));
+ return List.of(builderTypeName(targetType, task.outputPackage(), task.configuration()));
}
/** SPI-exports the registered builder on top of the generic generated-type registration. */
@@ -470,7 +465,6 @@ protected void onPlannedOutputRegistered(
protected BuilderDefinitionDto analyse(
CodegenTask task, BuilderProcessingContext ctx)
throws BuilderException {
- ctx.initProcessingTarget(new ProcessingTarget<>(task.configuration(), builderPackageOf(task)));
PerformanceTracker tracker = ctx.getPerformanceTracker();
tracker.startPhase();
BuilderDefinitionDto builderDef = extractFromElement(task.element(), ctx);
@@ -524,15 +518,6 @@ protected void afterGenerate(
definition.getBuilderTypeName().getClassName());
}
- /**
- * The package the builder is generated into: the configured {@code packageName} option when set,
- * otherwise the package of the element the generation is reported on - the
- * {@code @SimpleBuilderFor} holder for external types, the annotated type itself otherwise.
- */
- private String builderPackageOf(CodegenTask task) {
- return effectiveBuilderPackage(task.reportingElement(), task.configuration());
- }
-
/**
* The effective builder package: the configured {@code packageName} option when set, otherwise
* the package of the given element.
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
index c7182a5b..cb51fe49 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
@@ -39,6 +39,7 @@
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
+import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.simple.builders.processor.analysis.GeneratedTypes;
import org.javahelpers.simple.builders.processor.analysis.TypeMappingContext;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
@@ -83,6 +84,9 @@ public class AnnotationProcessingContext implements TypeMappingContext {
/** Package generated code is written to, or {@code null} when not generating into a package. */
private String generatedPackageName;
+ /** Per-target processing state for the type currently being generated. */
+ private ProcessingTarget> processingTarget;
+
/**
* Creates the context with default options (strict mode enabled, performance tracking disabled).
*
@@ -126,6 +130,28 @@ protected void setGeneratedPackageName(String generatedPackageName) {
this.generatedPackageName = generatedPackageName;
}
+ /**
+ * Initializes the per-target state ({@link ProcessingTarget}) for the type currently being
+ * generated. The processing driver calls this once per planned task before analysis starts, so
+ * downstream code can reach the current configuration and output package without threading both
+ * through every call.
+ *
+ * @param processingTarget the resolved configuration and output package of the current target
+ */
+ public void initProcessingTarget(ProcessingTarget> processingTarget) {
+ this.processingTarget = processingTarget;
+ setGeneratedPackageName(processingTarget.targetPackage());
+ }
+
+ /**
+ * The state of the current processing target.
+ *
+ * @return the current target's configuration and output package
+ */
+ public ProcessingTarget> getProcessingTarget() {
+ return processingTarget;
+ }
+
/**
* Checks whether a member (constructor, method) is accessible from the package generated code is
* written to.
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java
index d325683d..fbb41550 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java
@@ -28,7 +28,6 @@
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
-import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver;
import org.javahelpers.simple.builders.processor.generators.registry.GeneratorRegistry;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
@@ -47,7 +46,6 @@ public final class BuilderProcessingContext extends AnnotationProcessingContext
private final BuilderScopeResolver builderScopeResolver;
private GeneratorRegistry generatorRegistry;
- private ProcessingTarget processingTarget;
/**
* Creates a new processing context.
@@ -80,24 +78,13 @@ private static AnnotationProcessingOptions createOptions(
.build();
}
- /**
- * Initializes the per-target state ({@link ProcessingTarget}) for the type whose builder is
- * currently being generated. Called once per target before extraction starts.
- *
- * @param processingTarget the resolved configuration and builder package of the current target
- */
- public void initProcessingTarget(ProcessingTarget processingTarget) {
- this.processingTarget = processingTarget;
- setGeneratedPackageName(processingTarget.targetPackage());
- }
-
/**
* Gets the configuration for the current processing target.
*
* @return the builder configuration for the target being processed
*/
public BuilderConfiguration getConfiguration() {
- return this.processingTarget.configuration();
+ return (BuilderConfiguration) getProcessingTarget().configuration();
}
/**
@@ -106,7 +93,7 @@ public BuilderConfiguration getConfiguration() {
* @return the qualified package name of the generated builder
*/
public String getBuilderPackageName() {
- return this.processingTarget.targetPackage();
+ return getProcessingTarget().targetPackage();
}
/**
From e2d445111f5c1375c0503f4c47c1485f54d2f2cd Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:50:30 +0000
Subject: [PATCH 035/170] =?UTF-8?q?processing:=20driver=20wraps=20analyse/?=
=?UTF-8?q?map=20in=20phase=20calls=20=E2=80=94=20domains=20name=20them,?=
=?UTF-8?q?=20not=20call=20them?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 38 ++++++++++++++++++-
.../builders/processor/BuilderProcessor.java | 24 ++++++------
2 files changed, 49 insertions(+), 13 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 71538024..17fd793f 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -92,6 +92,12 @@ public abstract class AbstractCodegenProcessor<
/** Performance phase target generation is reported under — framework-owned. */
private static final String PHASE_CODE_GENERATION = "Code Generation";
+ /** Default performance phase for {@link #analyse} — domains may name their own. */
+ private static final String PHASE_ANALYSIS = "Analysis";
+
+ /** Default performance phase for {@link #mapToTargets} — domains may name their own. */
+ private static final String PHASE_TARGET_MAPPING = "Target Mapping";
+
/** The domain's context instance. */
protected C context;
@@ -179,8 +185,20 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
try {
context.initProcessingTarget(
new ProcessingTarget<>(task.configuration(), task.outputPackage()));
- D definition = analyse(task, context);
- List targets = mapToTargets(definition, task, context);
+ tracker.startPhase();
+ D definition;
+ try {
+ definition = analyse(task, context);
+ } finally {
+ tracker.endPhase(analysisPhase());
+ }
+ tracker.startPhase();
+ List targets;
+ try {
+ targets = mapToTargets(definition, task, context);
+ } finally {
+ tracker.endPhase(mappingPhase());
+ }
tracker.startPhase();
for (GenerationTargetClassDto target : targets) {
generateTarget(target);
@@ -499,6 +517,22 @@ protected boolean isFinished(C context) {
return processedOnce;
}
+ /**
+ * The performance phase {@link #analyse} is reported under — domain-named. Default {@value
+ * #PHASE_ANALYSIS}.
+ */
+ protected String analysisPhase() {
+ return PHASE_ANALYSIS;
+ }
+
+ /**
+ * The performance phase {@link #mapToTargets} is reported under — domain-named. Default {@value
+ * #PHASE_TARGET_MAPPING}.
+ */
+ protected String mappingPhase() {
+ return PHASE_TARGET_MAPPING;
+ }
+
/**
* Analyses one planned task into its domain definition — the analysis half of the
* analysis/generation separation.
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index e67aeaef..a8e88f42 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -461,15 +461,21 @@ protected void onPlannedOutputRegistered(
}
}
+ @Override
+ protected String analysisPhase() {
+ return PHASE_BUILDER_DEFINITION_EXTRACTION;
+ }
+
+ @Override
+ protected String mappingPhase() {
+ return PHASE_DTO_MAPPING;
+ }
+
@Override
protected BuilderDefinitionDto analyse(
CodegenTask task, BuilderProcessingContext ctx)
throws BuilderException {
- PerformanceTracker tracker = ctx.getPerformanceTracker();
- tracker.startPhase();
- BuilderDefinitionDto builderDef = extractFromElement(task.element(), ctx);
- tracker.endPhase(PHASE_BUILDER_DEFINITION_EXTRACTION);
- return builderDef;
+ return extractFromElement(task.element(), ctx);
}
@Override
@@ -477,12 +483,8 @@ protected List mapToTargets(
BuilderDefinitionDto definition,
CodegenTask task,
BuilderProcessingContext ctx) {
- PerformanceTracker tracker = ctx.getPerformanceTracker();
- tracker.startPhase();
- GenerationTargetClassDto renderingDto =
- new BuilderToGenerationTypeMapper(task.configuration()).toRenderingDto(definition);
- tracker.endPhase(PHASE_DTO_MAPPING);
- return List.of(renderingDto);
+ return List.of(
+ new BuilderToGenerationTypeMapper(task.configuration()).toRenderingDto(definition));
}
@Override
From 35db203e5ad03f985a685c8c5b02d3a0bf371633 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:50:38 +0000
Subject: [PATCH 036/170] =?UTF-8?q?processing:=20afterGenerate=20is=20an?=
=?UTF-8?q?=20event,=20not=20a=20step=20=E2=80=94=20renamed=20onTaskGenera?=
=?UTF-8?q?ted?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/AbstractCodegenProcessor.java | 12 ++++++------
.../simple/builders/processor/BuilderProcessor.java | 2 +-
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 17fd793f..7364ac09 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -67,7 +67,7 @@
* targets = mapToTargets(def, task, ctx) [implement]
* for each target:
* existsGuard → engine.generate(target, formatterResolver.resolve(target.mode)) [framework]
- * afterGenerate(def, targets, task, ctx) [default: no-op]
+ * onTaskGenerated(def, targets, task, ctx) [default: no-op]
* endRound(ctx) [default: no-op]
* last round: onProcessingOver(ctx) [default: deferredTargets() loop]
*
@@ -204,7 +204,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
generateTarget(target);
}
tracker.endPhase(PHASE_CODE_GENERATION);
- afterGenerate(definition, targets, task, context);
+ onTaskGenerated(definition, targets, task, context);
successfulGenerations++;
} catch (Exception ex) {
// By default generation failures are warnings so other targets are still generated.
@@ -558,15 +558,15 @@ protected abstract List mapToTargets(
D definition, CodegenTask task, C context) throws Exception;
/**
- * Runs after all targets of a task generated successfully — e.g. deferred-output accumulation and
- * per-class metrics. Default: nothing.
+ * Event after all targets of a task generated successfully — the insert point for deferred-output
+ * accumulation, per-class metrics, and domain bookkeeping. Default: nothing.
*
* @param definition the analysed definition
* @param targets the generated class definitions
* @param task the planned task
* @param context the processing context
*/
- protected void afterGenerate(
+ protected void onTaskGenerated(
D definition, List targets, CodegenTask task, C context) {}
/**
@@ -595,7 +595,7 @@ protected void onProcessingOver(C context) {
/**
* The class definitions generated deferred in the last round — accumulated during {@link
- * #afterGenerate}. Default: none.
+ * #onTaskGenerated}. Default: none.
*
* @param context the processing context
* @return the deferred targets, possibly empty
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index a8e88f42..95c59fde 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -488,7 +488,7 @@ protected List mapToTargets(
}
@Override
- protected void afterGenerate(
+ protected void onTaskGenerated(
BuilderDefinitionDto definition,
List targets,
CodegenTask task,
From ab103e72c2a168cf96965e55998a11f3894ca5e6 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:51:07 +0000
Subject: [PATCH 037/170] processing: specification and generation are two
sequential loops, not nested
---
.../processing/AbstractCodegenProcessor.java | 39 ++++++++++++++++---
1 file changed, 33 insertions(+), 6 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 7364ac09..d041ca72 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -24,6 +24,7 @@
package org.javahelpers.codegen.processing;
+import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
@@ -178,7 +179,8 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
registerPlannedOutputs(tasks, context);
- int successfulGenerations = 0;
+ // First loop: specification — analyse every task and map its render targets.
+ List> planned = new ArrayList<>();
for (CodegenTask task : tasks) {
context.debugStartOperation("Processing element: " + task.element().getSimpleName());
tracker.startClass(task.element().getSimpleName().toString());
@@ -199,18 +201,39 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
} finally {
tracker.endPhase(mappingPhase());
}
+ planned.add(new PlannedWork<>(task, definition, targets));
+ } catch (Exception ex) {
+ // By default generation failures are warnings so other targets are still generated.
+ // In opt-in strict mode they are promoted to errors that fail the build.
+ context.reportBasedOnStrictMode(
+ task.reportingElement(),
+ "%s: Failed to generate %s - %s",
+ processorTag(),
+ generatedKind(),
+ ex.getMessage());
+ } finally {
+ context.debugEndOperation();
+ }
+ }
+
+ // Second loop: generation — every specification exists before any file is written.
+ int successfulGenerations = 0;
+ for (PlannedWork work : planned) {
+ context.debugStartOperation(
+ "Generating targets for: " + work.task().element().getSimpleName());
+ try {
+ context.initProcessingTarget(
+ new ProcessingTarget<>(work.task().configuration(), work.task().outputPackage()));
tracker.startPhase();
- for (GenerationTargetClassDto target : targets) {
+ for (GenerationTargetClassDto target : work.targets()) {
generateTarget(target);
}
tracker.endPhase(PHASE_CODE_GENERATION);
- onTaskGenerated(definition, targets, task, context);
+ onTaskGenerated(work.definition(), work.targets(), work.task(), context);
successfulGenerations++;
} catch (Exception ex) {
- // By default generation failures are warnings so other targets are still generated.
- // In opt-in strict mode they are promoted to errors that fail the build.
context.reportBasedOnStrictMode(
- task.reportingElement(),
+ work.task().reportingElement(),
"%s: Failed to generate %s - %s",
processorTag(),
generatedKind(),
@@ -610,4 +633,8 @@ private String deferredTargetsKind() {
/** Callback at the end of a generating round — domain finalization. */
protected void endRound(C context) {}
+
+ /** One task's specification output, carried from the analysis loop into the generation loop. */
+ private record PlannedWork(
+ CodegenTask task, D definition, List targets) {}
}
From 5f7c89ec92f442945dfe8bc43ede578543592795 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:51:45 +0000
Subject: [PATCH 038/170] =?UTF-8?q?processing:=20deferred=20outputs=20are?=
=?UTF-8?q?=20pushed=20onto=20the=20context=20=E2=80=94=20an=20event,=20no?=
=?UTF-8?q?t=20a=20pulled=20hook?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 27 ++++---------
.../CodegenProcessorConfiguration.java | 24 +-----------
.../codegen/processing/DeferredTarget.java | 38 +++++++++++++++++++
.../builders/processor/BuilderProcessor.java | 11 +++---
.../AnnotationProcessingContext.java | 28 ++++++++++++++
5 files changed, 80 insertions(+), 48 deletions(-)
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/DeferredTarget.java
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index d041ca72..6ea3ab3d 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -593,16 +593,18 @@ protected void onTaskGenerated(
D definition, List targets, CodegenTask task, C context) {}
/**
- * Callback for the last processing round — generates the domain's {@link #deferredTargets
- * deferred targets} through the same guarded path as round targets. Domain extensions wrap this
- * with their finalization (SPI flush, reports).
+ * Callback for the last processing round — generates every {@link DeferredTarget} the domain
+ * pushed onto the context through the same guarded path as round targets. Domain extensions push
+ * their deferred outputs and call {@code super.onProcessingOver(context)} to drain, wrapped in
+ * their finalization (SPI flush, reports).
*
* @param context the processing context
*/
protected void onProcessingOver(C context) {
- for (GenerationTargetClassDto target : deferredTargets(context)) {
+ for (DeferredTarget deferred : context.getDeferredTargets()) {
+ GenerationTargetClassDto target = deferred.target();
String packageName = target.getTypeName().getPackageName();
- String kind = deferredTargetsKind();
+ String kind = deferred.kind();
context.info(
"%s: Generating %s in package '%s'",
processorTag(), Character.toUpperCase(kind.charAt(0)) + kind.substring(1), packageName);
@@ -616,21 +618,6 @@ protected void onProcessingOver(C context) {
}
}
- /**
- * The class definitions generated deferred in the last round — accumulated during {@link
- * #onTaskGenerated}. Default: none.
- *
- * @param context the processing context
- * @return the deferred targets, possibly empty
- */
- protected List deferredTargets(C context) {
- return List.of();
- }
-
- private String deferredTargetsKind() {
- return processorConfiguration.deferredTargetsKind();
- }
-
/** Callback at the end of a generating round — domain finalization. */
protected void endRound(C context) {}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
index f0235752..f122f938 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
@@ -41,16 +41,13 @@
* @param processorTag diagnostic tag prefixing messages; default: {@code null} — the processor's
* simple class name is used
* @param generatedKind what one generated unit is called in diagnostics; default: {@code "class"}
- * @param deferredTargetsKind what deferred targets are called in diagnostics; default: {@code
- * "deferred target"}
*/
public record CodegenProcessorConfiguration(
AnnotationElementSpec elementSpec,
ProcessorOptionRegistry optionRegistry,
SourceVersion requiredSourceVersion,
String processorTag,
- String generatedKind,
- String deferredTargetsKind) {
+ String generatedKind) {
/**
* Creates a builder with all defaults applied — only {@link #elementSpec(AnnotationElementSpec)}
@@ -69,7 +66,6 @@ public static final class Builder {
private SourceVersion requiredSourceVersion = SourceVersion.latestSupported();
private String processorTag;
private String generatedKind = "class";
- private String deferredTargetsKind = "deferred target";
private Builder() {}
@@ -128,17 +124,6 @@ public Builder generatedKind(String generatedKind) {
return this;
}
- /**
- * Sets what deferred targets are called in diagnostics.
- *
- * @param deferredTargetsKind the deferred targets kind
- * @return this builder
- */
- public Builder deferredTargetsKind(String deferredTargetsKind) {
- this.deferredTargetsKind = deferredTargetsKind;
- return this;
- }
-
/**
* Builds the configuration.
*
@@ -150,12 +135,7 @@ public CodegenProcessorConfiguration build() {
throw new IllegalStateException("elementSpec is required");
}
return new CodegenProcessorConfiguration(
- elementSpec,
- optionRegistry,
- requiredSourceVersion,
- processorTag,
- generatedKind,
- deferredTargetsKind);
+ elementSpec, optionRegistry, requiredSourceVersion, processorTag, generatedKind);
}
}
}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/DeferredTarget.java b/processor/src/main/java/org/javahelpers/codegen/processing/DeferredTarget.java
new file mode 100644
index 00000000..5e833960
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/DeferredTarget.java
@@ -0,0 +1,38 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
+
+/**
+ * A class definition the domain wants generated deferred — pushed onto the processing context
+ * during a round (typically from {@code onTaskGenerated} or the domain's last-round callback) and
+ * generated by the driver in the last round through the same guarded path as round targets.
+ *
+ * @param target the class definition to generate
+ * @param kind what the deferred output is called in diagnostics (a domain noun like "Jackson
+ * module")
+ */
+public record DeferredTarget(GenerationTargetClassDto target, String kind) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 95c59fde..edc8e088 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -54,6 +54,7 @@
import org.javahelpers.codegen.processing.AnnotationElementSpec;
import org.javahelpers.codegen.processing.CodegenProcessorConfiguration;
import org.javahelpers.codegen.processing.CodegenTask;
+import org.javahelpers.codegen.processing.DeferredTarget;
import org.javahelpers.codegen.processing.ElementCollection;
import org.javahelpers.codegen.processing.ProcessorOptionRegistry;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
@@ -128,7 +129,6 @@ public class BuilderProcessor
.requiredSourceVersion(SourceVersion.RELEASE_17)
.processorTag("simple-builders")
.generatedKind("builder")
- .deferredTargetsKind("Jackson module")
.build();
/** Creates the processor — javac instantiates it reflectively. */
@@ -529,16 +529,15 @@ private String effectiveBuilderPackage(Element reportingElement, BuilderConfigur
return configured == null ? context.getPackageName(reportingElement) : configured;
}
- @Override
- protected List deferredTargets(BuilderProcessingContext ctx) {
- return jacksonModuleGenerator.getModuleDefinitions();
- }
-
/** Generates all Jackson modules after the last processing round and reports the metrics. */
@Override
protected void onProcessingOver(BuilderProcessingContext ctx) {
SimpleBuildersSpiIntegration.finishCompilation();
ctx.getPerformanceTracker().generateReport(logger);
+ for (GenerationTargetClassDto moduleDefinition :
+ jacksonModuleGenerator.getModuleDefinitions()) {
+ ctx.addDeferredTarget(new DeferredTarget(moduleDefinition, "Jackson module"));
+ }
super.onProcessingOver(ctx);
// Reset indentation after Jackson module generation as well
ctx.resetIndentation();
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
index cb51fe49..79337dad 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
@@ -24,6 +24,7 @@
package org.javahelpers.simple.builders.processor.processing;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -39,6 +40,7 @@
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
+import org.javahelpers.codegen.processing.DeferredTarget;
import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.simple.builders.processor.analysis.GeneratedTypes;
import org.javahelpers.simple.builders.processor.analysis.TypeMappingContext;
@@ -87,6 +89,12 @@ public class AnnotationProcessingContext implements TypeMappingContext {
/** Per-target processing state for the type currently being generated. */
private ProcessingTarget> processingTarget;
+ /**
+ * Class definitions the domain pushed for deferred last-round generation — drained by the
+ * processing driver in the last round.
+ */
+ private final List deferredTargets = new ArrayList<>();
+
/**
* Creates the context with default options (strict mode enabled, performance tracking disabled).
*
@@ -152,6 +160,26 @@ public ProcessingTarget> getProcessingTarget() {
return processingTarget;
}
+ /**
+ * Queues a class definition for deferred generation in the last processing round. Domains push
+ * during a round ({@code onTaskGenerated}, end-of-round or finalization events) and the driver
+ * generates them once specification of every target has finished.
+ *
+ * @param target the deferred output and its diagnostic kind
+ */
+ public void addDeferredTarget(DeferredTarget target) {
+ deferredTargets.add(target);
+ }
+
+ /**
+ * The deferred outputs queued so far.
+ *
+ * @return the mutable deferred-target list
+ */
+ public List getDeferredTargets() {
+ return deferredTargets;
+ }
+
/**
* Checks whether a member (constructor, method) is accessible from the package generated code is
* written to.
From 69f42b6a078539df933a646495ed06c897acfe82 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:52:33 +0000
Subject: [PATCH 039/170] processing: the logger carries the tag; domain nouns
live in domain code
---
.../processing/AbstractCodegenProcessor.java | 30 ++++++++++---
.../CodegenProcessorConfiguration.java | 34 +-------------
.../builders/processor/BuilderProcessor.java | 14 +++++-
.../processing/logging/ProcessingLogger.java | 45 +++++++++++++++++--
4 files changed, 79 insertions(+), 44 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 6ea3ab3d..e76f560f 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -364,16 +364,27 @@ private void assertTargetDoesNotExist(GenerationTargetClassDto target)
}
}
+ /**
+ * The diagnostic tag the processor's messages are prefixed with — carried by the logger. Default:
+ * the processor's simple class name; override for a shorter domain tag.
+ *
+ * @return the tag
+ */
+ protected String loggerTag() {
+ return getClass().getSimpleName();
+ }
+
/**
* Creates the logger. Default: verbose when the domain's declared verbose option requests it —
- * see {@link ProcessorOptionRegistry#isVerboseRequested}.
+ * see {@link ProcessorOptionRegistry#isVerboseRequested}. The logger carries {@link
+ * #loggerTag()}.
*/
protected ProcessingLogger createLogger(
ProcessingEnvironment processingEnv, CompilerArgumentsReader reader) {
ProcessingLogger logger =
optionRegistry().isVerboseRequested(reader)
- ? ProcessingLogger.createVerbose(processingEnv)
- : ProcessingLogger.create(processingEnv);
+ ? ProcessingLogger.createVerbose(processingEnv, loggerTag())
+ : ProcessingLogger.create(processingEnv, loggerTag());
logger.debug("Starting %s...", getClass().getSimpleName());
return logger;
}
@@ -447,12 +458,17 @@ private SourceVersion requiredSourceVersion() {
}
private String processorTag() {
- String configured = processorConfiguration.processorTag();
- return configured != null ? configured : getClass().getSimpleName();
+ return logger.getTag();
}
- private String generatedKind() {
- return processorConfiguration.generatedKind();
+ /**
+ * What one generated unit is called in diagnostics ("Failed to generate %s", "generated N %s(s)")
+ * — the domain's noun lives in domain code. Default {@code "class"}.
+ *
+ * @return the generated unit's name
+ */
+ protected String generatedKind() {
+ return "class";
}
/**
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
index f122f938..f7efcba0 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
@@ -38,16 +38,11 @@
* @param optionRegistry the processor's declared compiler options; default: an empty registry
* @param requiredSourceVersion the minimum source level; default: {@link
* SourceVersion#latestSupported()}
- * @param processorTag diagnostic tag prefixing messages; default: {@code null} — the processor's
- * simple class name is used
- * @param generatedKind what one generated unit is called in diagnostics; default: {@code "class"}
*/
public record CodegenProcessorConfiguration(
AnnotationElementSpec elementSpec,
ProcessorOptionRegistry optionRegistry,
- SourceVersion requiredSourceVersion,
- String processorTag,
- String generatedKind) {
+ SourceVersion requiredSourceVersion) {
/**
* Creates a builder with all defaults applied — only {@link #elementSpec(AnnotationElementSpec)}
@@ -64,8 +59,6 @@ public static final class Builder {
private AnnotationElementSpec elementSpec;
private ProcessorOptionRegistry optionRegistry = new ProcessorOptionRegistry(List.of());
private SourceVersion requiredSourceVersion = SourceVersion.latestSupported();
- private String processorTag;
- private String generatedKind = "class";
private Builder() {}
@@ -102,28 +95,6 @@ public Builder requiredSourceVersion(SourceVersion requiredSourceVersion) {
return this;
}
- /**
- * Sets the diagnostic tag prefixing messages.
- *
- * @param processorTag the processor tag
- * @return this builder
- */
- public Builder processorTag(String processorTag) {
- this.processorTag = processorTag;
- return this;
- }
-
- /**
- * Sets what one generated unit is called in diagnostics.
- *
- * @param generatedKind the generated kind
- * @return this builder
- */
- public Builder generatedKind(String generatedKind) {
- this.generatedKind = generatedKind;
- return this;
- }
-
/**
* Builds the configuration.
*
@@ -134,8 +105,7 @@ public CodegenProcessorConfiguration build() {
if (elementSpec == null) {
throw new IllegalStateException("elementSpec is required");
}
- return new CodegenProcessorConfiguration(
- elementSpec, optionRegistry, requiredSourceVersion, processorTag, generatedKind);
+ return new CodegenProcessorConfiguration(elementSpec, optionRegistry, requiredSourceVersion);
}
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index edc8e088..fde9e40b 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -127,8 +127,6 @@ public class BuilderProcessor
.elementSpec(ELEMENT_SPEC)
.optionRegistry(OPTION_REGISTRY)
.requiredSourceVersion(SourceVersion.RELEASE_17)
- .processorTag("simple-builders")
- .generatedKind("builder")
.build();
/** Creates the processor — javac instantiates it reflectively. */
@@ -136,6 +134,18 @@ public BuilderProcessor() {
super(PROCESSOR_CONFIGURATION);
}
+ /** The tag prefixing diagnostics — the project name, not the class name. */
+ @Override
+ protected String loggerTag() {
+ return "simple-builders";
+ }
+
+ /** Builders are what this processor generates — the noun diagnostics use. */
+ @Override
+ protected String generatedKind() {
+ return "builder";
+ }
+
/** The global configuration resolved from compiler arguments — needed by several hooks. */
private BuilderConfiguration globalConfig;
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
index 171eb9bb..4b705dc5 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
@@ -41,6 +41,12 @@ public class ProcessingLogger {
/** Verbosity level of this logger. */
private final Verbosity verbosity;
+ /**
+ * Diagnostic tag identifying the processor — domains read it back via {@link #getTag()} to prefix
+ * their own messages.
+ */
+ private final String tag;
+
/** Thread-local indentation level for hierarchical logging. */
private static final ThreadLocal indentationLevel = ThreadLocal.withInitial(() -> 0);
@@ -61,7 +67,18 @@ private enum Verbosity {
* @return a processing logger reporting errors, warnings and info messages
*/
public static ProcessingLogger create(ProcessingEnvironment processingEnv) {
- return new ProcessingLogger(processingEnv, Verbosity.NORMAL);
+ return new ProcessingLogger(processingEnv, Verbosity.NORMAL, null);
+ }
+
+ /**
+ * Creates a new ProcessingLogger carrying a diagnostic tag.
+ *
+ * @param processingEnv the processing environment providing the messager
+ * @param tag the processor's diagnostic tag, or {@code null}
+ * @return a processing logger reporting errors, warnings and info messages
+ */
+ public static ProcessingLogger create(ProcessingEnvironment processingEnv, String tag) {
+ return new ProcessingLogger(processingEnv, Verbosity.NORMAL, tag);
}
/**
@@ -71,15 +88,37 @@ public static ProcessingLogger create(ProcessingEnvironment processingEnv) {
* @return a debug-enabled processing logger
*/
public static ProcessingLogger createVerbose(ProcessingEnvironment processingEnv) {
- return new ProcessingLogger(processingEnv, Verbosity.VERBOSE);
+ return new ProcessingLogger(processingEnv, Verbosity.VERBOSE, null);
+ }
+
+ /**
+ * Creates a new debug-enabled ProcessingLogger carrying a diagnostic tag.
+ *
+ * @param processingEnv the processing environment providing the messager
+ * @param tag the processor's diagnostic tag, or {@code null}
+ * @return a debug-enabled processing logger
+ */
+ public static ProcessingLogger createVerbose(ProcessingEnvironment processingEnv, String tag) {
+ return new ProcessingLogger(processingEnv, Verbosity.VERBOSE, tag);
}
- private ProcessingLogger(ProcessingEnvironment processingEnv, Verbosity verbosity) {
+ private ProcessingLogger(ProcessingEnvironment processingEnv, Verbosity verbosity, String tag) {
// Reset ThreadLocal state to ensure clean state between test runs
resetThreadLocalState();
this.messager = processingEnv.getMessager();
this.verbosity = verbosity;
+ this.tag = tag;
+ }
+
+ /**
+ * The diagnostic tag this logger was created with (typically the processor's name or a shorter
+ * domain tag), or {@code null}.
+ *
+ * @return the tag
+ */
+ public String getTag() {
+ return tag;
}
/** Resets ThreadLocal state to ensure clean state between test runs. */
From f4efd83a27d0bf50b5cf7cc6b23a2be51e90753a Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:53:05 +0000
Subject: [PATCH 040/170] =?UTF-8?q?processing:=20required=20source=20versi?=
=?UTF-8?q?on=20by=20name=20=E2=80=94=20the=20graceful-guard=20property=20?=
=?UTF-8?q?of=20the=20original=20JDK=20check?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 38 +++++++++++++------
.../CodegenProcessorConfiguration.java | 18 +++++----
.../builders/processor/BuilderProcessor.java | 3 +-
3 files changed, 37 insertions(+), 22 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index e76f560f..b38f4a56 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -51,26 +51,27 @@
* the domain plugs in its hooks — collect, plan, register, analyse/map, generate, finalize.
*
* A new processor declares its static surface once as {@link CodegenProcessorConfiguration}
- * (passed to the constructor — element spec, options, source level, diagnostic names) and
- * implements only the domain hooks marked {@code [implement]}: {@link #createContext}, {@link
- * #plan}, {@link #predictedGeneratedTypeName}, {@link #analyse} and {@link #mapToTargets}. Every
- * other step is framework-owned or an optional {@code [default]} the domain may override.
+ * (passed to the constructor — element spec, options, source level) and implements only the domain
+ * hooks marked {@code [implement]}: {@link #createContext}, {@link #planTasks}, {@link
+ * #registerPlannedGenerationTargets}, {@link #analyse} and {@link #mapToTargets}. Every other step
+ * is framework-owned or an optional {@code [default]} the domain may override.
*
*
The round spine:
*
*
* ctx = createContext(env, reader, logger) [implement — in init]
* elements = findElements(annotations, roundEnv, ctx) [default: configuration's elementSpec]
- * tasks = plan(elements, ctx) [implement]
- * registerPlannedOutputs(tasks, ctx) [default: registerPlannedGenerationTargets() — implement]
- * for each task (guarded, strict-mode reporting): [framework]
+ * tasks = planTasks(elements, ctx) [implement]
+ * registerPlannedOutputs(tasks, ctx) [framework; per-task names from registerPlannedGenerationTargets() — implement]
+ * loop 1 — specification (guarded, strict-mode reporting): [framework]
* def = analyse(task, ctx) [implement]
* targets = mapToTargets(def, task, ctx) [implement]
+ * loop 2 — generation: [framework]
* for each target:
* existsGuard → engine.generate(target, formatterResolver.resolve(target.mode)) [framework]
- * onTaskGenerated(def, targets, task, ctx) [default: no-op]
+ * onTaskGenerated(def, targets, task, ctx) [default: no-op]
* endRound(ctx) [default: no-op]
- * last round: onProcessingOver(ctx) [default: deferredTargets() loop]
+ * last round: onProcessingOver(ctx) [default: drains context.getDeferredTargets()]
*
*
* Per-element failures are caught and reported through {@link
@@ -132,9 +133,9 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
this.formatterResolver = createFormatterResolver(reader, logger);
SourceVersion current = processingEnv.getSourceVersion();
- this.supportedJdk = current.ordinal() >= requiredSourceVersion().ordinal();
+ this.supportedJdk = isAtLeast(current, requiredSourceVersion());
if (!this.supportedJdk) {
- String releaseNumber = requiredSourceVersion().toString().replace("RELEASE_", "");
+ String releaseNumber = requiredSourceVersion().replace("RELEASE_", "");
context.error(
"%s requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor.",
processorTag(), releaseNumber, current, releaseNumber);
@@ -453,10 +454,23 @@ protected boolean claimsAnnotations(Set supportedTypes) {
return false;
}
- private SourceVersion requiredSourceVersion() {
+ private String requiredSourceVersion() {
return processorConfiguration.requiredSourceVersion();
}
+ /**
+ * Whether {@code current} meets the named minimum. The name resolves inside the check so a JDK
+ * whose {@link SourceVersion} lacks the constant (e.g. a processor built on a newer JDK running
+ * under an older one) reports the graceful error path instead of a linkage failure.
+ */
+ private static boolean isAtLeast(SourceVersion current, String requiredName) {
+ try {
+ return current.ordinal() >= SourceVersion.valueOf(requiredName).ordinal();
+ } catch (IllegalArgumentException ex) {
+ return false;
+ }
+ }
+
private String processorTag() {
return logger.getTag();
}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
index f7efcba0..e8712075 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
@@ -25,7 +25,6 @@
package org.javahelpers.codegen.processing;
import java.util.List;
-import javax.lang.model.SourceVersion;
/**
* Static declarations of an {@link AbstractCodegenProcessor}: everything the framework needs to
@@ -36,13 +35,13 @@
*
* @param elementSpec which elements the domain generates code for (required)
* @param optionRegistry the processor's declared compiler options; default: an empty registry
- * @param requiredSourceVersion the minimum source level; default: {@link
- * SourceVersion#latestSupported()}
+ * @param requiredSourceVersion the minimum source level by name (e.g. {@code "RELEASE_17"});
+ * default: the running JDK's latest supported release
*/
public record CodegenProcessorConfiguration(
AnnotationElementSpec elementSpec,
ProcessorOptionRegistry optionRegistry,
- SourceVersion requiredSourceVersion) {
+ String requiredSourceVersion) {
/**
* Creates a builder with all defaults applied — only {@link #elementSpec(AnnotationElementSpec)}
@@ -58,7 +57,7 @@ public static Builder builder() {
public static final class Builder {
private AnnotationElementSpec elementSpec;
private ProcessorOptionRegistry optionRegistry = new ProcessorOptionRegistry(List.of());
- private SourceVersion requiredSourceVersion = SourceVersion.latestSupported();
+ private String requiredSourceVersion;
private Builder() {}
@@ -85,12 +84,15 @@ public Builder optionRegistry(ProcessorOptionRegistry optionRegistry) {
}
/**
- * Sets the minimum source level the processor supports.
+ * Sets the minimum source level the processor supports, by {@code SourceVersion} constant name
+ * (e.g. {@code "RELEASE_17"}). A name — not the enum constant — keeps the check resolvable at
+ * processing time: a JDK whose {@code SourceVersion} lacks the constant still reports a
+ * graceful error instead of a linkage failure.
*
- * @param requiredSourceVersion the required source version
+ * @param requiredSourceVersion the required source version name
* @return this builder
*/
- public Builder requiredSourceVersion(SourceVersion requiredSourceVersion) {
+ public Builder requiredSourceVersion(String requiredSourceVersion) {
this.requiredSourceVersion = requiredSourceVersion;
return this;
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index fde9e40b..03c3853c 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -41,7 +41,6 @@
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
-import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
@@ -126,7 +125,7 @@ public class BuilderProcessor
CodegenProcessorConfiguration.builder()
.elementSpec(ELEMENT_SPEC)
.optionRegistry(OPTION_REGISTRY)
- .requiredSourceVersion(SourceVersion.RELEASE_17)
+ .requiredSourceVersion("RELEASE_17")
.build();
/** Creates the processor — javac instantiates it reflectively. */
From 93ccf4d66cc4d12c57d6419dd10d79901a7f2a30 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:53:42 +0000
Subject: [PATCH 041/170] =?UTF-8?q?processing:=20plan=20is=20the=20managem?=
=?UTF-8?q?ent=20function=20=E2=80=94=20gate=20and=20registration=20live?=
=?UTF-8?q?=20outside=20the=20domain=20hook?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 34 ++++++++++++++-----
.../builders/processor/BuilderProcessor.java | 2 +-
2 files changed, 26 insertions(+), 10 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index b38f4a56..560248aa 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -169,16 +169,9 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
context.debug("%s: Processing round started.", processorTag());
List> tasks = plan(collected, context);
- if (collected.isEmpty() || isFinished(context)) {
+ if (tasks.isEmpty()) {
return claimsAnnotations(supportedAnnotationTypes());
}
- if (!tasks.isEmpty()) {
- // The default one-shot is consumed by the first round that planned work — an empty round
- // leaves it open for a later one carrying matching elements.
- processedOnce = true;
- }
-
- registerPlannedOutputs(tasks, context);
// First loop: specification — analyse every task and map its render targets.
List> planned = new ArrayList<>();
@@ -500,6 +493,29 @@ protected ElementCollection findElements(
return elementSpec().collect(annotations, roundEnv, logger);
}
+ /**
+ * The planning step — runs the domain's {@link #planTasks}, applies the finished gate and the
+ * one-shot marker, and registers the round's planned outputs. Framework bookkeeping lives here so
+ * the domain function below stays pure planning.
+ *
+ * @param collected the elements matched by {@link #findElements}
+ * @param context the processing context
+ * @return the round's tasks, or empty when the round is gated out
+ */
+ private List> plan(ElementCollection collected, C context) {
+ List> tasks = planTasks(collected, context);
+ if (collected.isEmpty() || isFinished(context)) {
+ return List.of();
+ }
+ if (!tasks.isEmpty()) {
+ // The default one-shot is consumed by the first round that planned work — an empty round
+ // leaves it open for a later one carrying matching elements.
+ processedOnce = true;
+ }
+ registerPlannedOutputs(tasks, context);
+ return tasks;
+ }
+
/**
* Plans the tasks of this round from the collected elements: resolves per-element configuration,
* expands multi-target declarations and applies domain scoping rules.
@@ -508,7 +524,7 @@ protected ElementCollection findElements(
* @param context the processing context
* @return the generation plan, possibly empty
*/
- protected abstract List> plan(ElementCollection collected, C context);
+ protected abstract List> planTasks(ElementCollection collected, C context);
/**
* Registers the types this round will generate, up front, so references to them resolve during
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 03c3853c..c2a29930 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -173,7 +173,7 @@ protected BuilderProcessingContext createContext(
}
@Override
- protected List> plan(
+ protected List> planTasks(
ElementCollection collected, BuilderProcessingContext ctx) {
List sortedElements = collected.byName(GROUP_ANNOTATED_ELEMENTS);
List sortedHolders = collected.byName(GROUP_EXTERNAL_TYPE_HOLDERS);
From d227a0c5dd90ca0dc1d72649f9cc0ebc7325dd38 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:54:47 +0000
Subject: [PATCH 042/170] =?UTF-8?q?processing:=20the=20SPI=20lifecycle=20i?=
=?UTF-8?q?s=20generic=20=E2=80=94=20the=20domain=20delegates=20and=20keep?=
=?UTF-8?q?s=20only=20its=20published=20builders?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/ProcessorSpiIntegration.java | 113 ++++++++++++++++++
.../SimpleBuildersSpiIntegration.java | 74 +++---------
.../SimpleBuildersSpiIntegrationTest.java | 2 +-
3 files changed, 130 insertions(+), 59 deletions(-)
create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/ProcessorSpiIntegration.java
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorSpiIntegration.java b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorSpiIntegration.java
new file mode 100644
index 00000000..d412b6df
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorSpiIntegration.java
@@ -0,0 +1,113 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025-2026 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.codegen.processing;
+
+import java.util.concurrent.atomic.AtomicReference;
+import javax.lang.model.util.Elements;
+
+/**
+ * The lifecycle an SPI integration shares between a processor and the SPI adapters of other
+ * frameworks on the same annotation-processor path (and therefore the same classloader).
+ *
+ * One instance is owned by the processor's domain holder (usually a static field). The processor
+ * transitions it alone — SPI adapters only ever read it, they never initialize or mutate it. All
+ * state is compilation-scoped: javac initializes each processor lazily when its turn in a round
+ * comes, so before the processor has run its {@code init()} nothing can be trusted — values may be
+ * leftovers of a previous compilation in a long-lived JVM (Gradle daemon, incremental builds).
+ * Adapters therefore pass their own {@link Elements} instance to every read: javac hands every
+ * processor of one compilation the same {@code Elements} instance, so a mismatch means this holder
+ * still describes an older run.
+ */
+public final class ProcessorSpiIntegration {
+
+ /**
+ * Where the owning processor's generation stands in the current compilation, as SPI adapters
+ * observe it.
+ */
+ public enum State {
+ /**
+ * No processor init happened in this compilation yet — published content may be a previous
+ * run's, and the registry must be treated as possibly still filling.
+ */
+ INIT,
+ /**
+ * The processor initialized: its generating round may not have run yet — the registry must be
+ * treated as possibly still filling.
+ */
+ PROCESSING,
+ /** The generating round ran: the registry is final for this compilation. */
+ FINISHED
+ }
+
+ private volatile State state = State.INIT;
+
+ /** The {@link Elements} of the compilation this holder's content describes. */
+ private final AtomicReference compilationElements = new AtomicReference<>();
+
+ /** Starts a new compilation: marks generation as running for the given compilation. */
+ public void initCompilation(Elements elements) {
+ compilationElements.set(elements);
+ state = State.PROCESSING;
+ }
+
+ /** Marks generation final: the generating round ran (or the compilation ended without one). */
+ public void finishCompilation() {
+ state = State.FINISHED;
+ }
+
+ /**
+ * Whether {@code observed} belongs to the compilation this holder currently describes — {@code
+ * false} while it still carries a previous run's leftovers. javac creates one {@link Elements}
+ * per compilation and shares it between all processors, so reference identity is a reliable
+ * staleness check that never requires the SPI adapters to write anything.
+ */
+ public boolean isCurrentCompilation(Elements observed) {
+ return observed != null && observed == compilationElements.get();
+ }
+
+ /** The lifecycle state of the current compilation. */
+ public State state() {
+ return state;
+ }
+
+ /**
+ * Whether the owning processor finished generating for the compilation {@code observed} belongs
+ * to — {@code false} while this holder still describes another run.
+ *
+ * The guard is not optional: an SPI holder's statics live in the processor-path classloader,
+ * which build tools reuse across compilations (Gradle daemon, in-process javac, IDE builds,
+ * repeated compiles in one JVM). In a new compilation's first round — before the owning
+ * processor's {@code init()} had its turn — {@code state} can still be the previous run's {@link
+ * State#FINISHED} and the registry still holds its entries. Trusting them would claim outputs by
+ * name they were never planned with here and answer misses as final instead of deferring. javac
+ * hands every processor of one compilation the same {@link Elements} instance and a different one
+ * per compilation, so identity is the read-only, order-independent boundary marker — an SPI-side
+ * reset cannot work, because SPI init order against the owning processor's init is path-order
+ * dependent.
+ */
+ public boolean isFinishedForIntegration(Elements observed) {
+ return isCurrentCompilation(observed) && state == State.FINISHED;
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegration.java
index adc360d8..2e1da684 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegration.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegration.java
@@ -27,8 +27,9 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
-import java.util.concurrent.atomic.AtomicReference;
import javax.lang.model.util.Elements;
+import org.javahelpers.codegen.processing.ProcessorSpiIntegration;
+import org.javahelpers.codegen.processing.ProcessorSpiIntegration.State;
import org.javahelpers.simple.builders.processor.model.type.ResolvedBuilder;
import org.javahelpers.simple.builders.processor.model.type.TypeInstantiation.StaticFactoryCall;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
@@ -39,39 +40,14 @@
* resolved builder's type, creation and build method names plus the bean's configured {@code
* setterPrefix}, so adapters do not scan candidates or read annotation configuration themselves.
*
- *
The lifecycle reports where {@link BuilderProcessor}'s builder generation stands and is
- * transitioned by that processor alone — SPI adapters only ever read this holder, they never
- * initialize or mutate it. All state is compilation-scoped: javac initializes each processor lazily
- * when its turn in a round comes, so before {@link BuilderProcessor} has run its {@code init()}
- * nothing static can be trusted — values may be leftovers of a previous compilation in a long-lived
- * JVM (Gradle daemon, incremental builds). Adapters therefore pass their own {@link Elements}
- * instance to every read: javac hands every processor of one compilation the same {@code Elements}
- * instance, so a mismatch means this holder still describes an older run.
+ *
The lifecycle — where {@link BuilderProcessor}'s builder generation stands — is delegated to a
+ * {@link ProcessorSpiIntegration} instance and transitioned by the processor alone: SPI adapters
+ * only ever read this holder, they never initialize or mutate it.
*/
public final class SimpleBuildersSpiIntegration {
- /**
- * Where {@link BuilderProcessor}'s builder generation stands in the current compilation, as the
- * SPI adapters observe it.
- */
- public enum State {
- /**
- * No processor init happened in this compilation yet — static content may be a previous run's,
- * and the registry must be treated as possibly still filling.
- */
- INIT,
- /**
- * {@code BuilderProcessor} initialized: its single generating round may not have run yet — the
- * registry must be treated as possibly still filling.
- */
- PROCESSING,
- /**
- * The generating round ran: the registry is final for this compilation. {@code
- * BuilderProcessor} generates in exactly one round — the first round carrying its annotations —
- * so elements first appearing in later rounds get no builder.
- */
- FINISHED
- }
+ /** The processor-lifecycle holder this domain's SPI integration delegates to. */
+ private static final ProcessorSpiIntegration LIFECYCLE = new ProcessorSpiIntegration();
/**
* One bean-to-builder pair resolved at registration time: the type this processor emits for
@@ -85,11 +61,6 @@ public record PublishedBuilder(
String buildMethodName,
String setterPrefix) {}
- private static volatile State state = State.INIT;
-
- /** The {@link Elements} of the compilation this holder's content describes. */
- private static final AtomicReference compilationElements = new AtomicReference<>();
-
/** Builders published for the current compilation, keyed by bean qualified name. */
private static final Map BY_BEAN = new HashMap<>();
@@ -102,8 +73,7 @@ private SimpleBuildersSpiIntegration() {}
static void initCompilation(Elements elements) {
BY_BEAN.clear();
BY_BUILDER.clear();
- compilationElements.set(elements);
- state = State.PROCESSING;
+ LIFECYCLE.initCompilation(elements);
}
/**
@@ -111,41 +81,29 @@ static void initCompilation(Elements elements) {
* compilation ended without one) — no more builders will be published.
*/
static void finishCompilation() {
- state = State.FINISHED;
+ LIFECYCLE.finishCompilation();
}
/**
- * Whether {@code observed} belongs to the compilation this holder currently describes — {@code
- * false} while it still carries a previous run's leftovers. javac creates one {@link Elements}
- * per compilation and shares it between all processors, so reference identity is a reliable
- * staleness check that never requires the SPI adapters to write anything.
+ * Whether {@code observed} belongs to the compilation this holder currently describes — see
+ * {@link ProcessorSpiIntegration#isCurrentCompilation}.
*/
static boolean isCurrentCompilation(Elements observed) {
- return observed != null && observed == compilationElements.get();
+ return LIFECYCLE.isCurrentCompilation(observed);
}
/** The lifecycle state of the current compilation. */
static State state() {
- return state;
+ return LIFECYCLE.state();
}
/**
* Whether {@link BuilderProcessor} finished generating builders for the compilation {@code
- * observed} belongs to — {@code false} while this holder still describes another run.
- *
- * The guard is not optional: this holder's statics live in the processor-path classloader,
- * which build tools reuse across compilations (Gradle daemon, in-process javac, IDE builds,
- * repeated compiles in one JVM). In a new compilation's first round — before {@link
- * BuilderProcessor}'s {@code init()} had its turn — {@code state} can still be the previous run's
- * {@link State#FINISHED} and the registry still holds its entries. Trusting them would claim
- * beans by qualified name they were never planned with here and answer misses as final instead of
- * deferring. javac hands every processor of one compilation the same {@link Elements} instance
- * and a different one per compilation, so identity is the read-only, order-independent boundary
- * marker — an SPI-side reset cannot work, because SPI init order against {@link
- * BuilderProcessor}'s init is path-order dependent.
+ * observed} belongs to — {@code false} while this holder still describes another run. See {@link
+ * ProcessorSpiIntegration#isFinishedForIntegration} for the staleness semantics.
*/
public static boolean isSimpleBuildersFinishedForIntegration(Elements observed) {
- return isCurrentCompilation(observed) && state == State.FINISHED;
+ return LIFECYCLE.isFinishedForIntegration(observed);
}
/**
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegrationTest.java
index 47b772b9..11bee26c 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegrationTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegrationTest.java
@@ -31,8 +31,8 @@
import java.lang.reflect.Proxy;
import java.util.Optional;
import javax.lang.model.util.Elements;
+import org.javahelpers.codegen.processing.ProcessorSpiIntegration.State;
import org.javahelpers.simple.builders.processor.SimpleBuildersSpiIntegration.PublishedBuilder;
-import org.javahelpers.simple.builders.processor.SimpleBuildersSpiIntegration.State;
import org.javahelpers.simple.builders.processor.model.type.ResolvedBuilder;
import org.javahelpers.simple.builders.processor.model.type.TypeInstantiation.StaticFactoryCall;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
From eb3f29096e66fb9faa8b881170ca5a1e35513237 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 17:57:08 +0000
Subject: [PATCH 043/170] processor: expected log gains the generation-loop
header the two loops introduce
---
docs/CONTRIBUTING.md | 1 +
docs/DEBUG_LOGGING.md | 21 +++++++++++--------
.../processor/BuilderProcessorTest.java | 1 +
3 files changed, 14 insertions(+), 9 deletions(-)
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 11013bca..6fddef63 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -250,6 +250,7 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md).
[DEBUG] │ │ ├─ Analyzing method: setName(java.lang.String)
[DEBUG] │ │ │ └─ Adding field: name (type: java.lang.String)
[DEBUG] │ └─ Processed 1 possible setters: added 1 fields, skipped 0
+[DEBUG] Generating targets for: Project
[DEBUG] ├─ Code generation for builder: ProjectBuilder
[DEBUG] │ ├─ Adding Methods for 4 candidates
[DEBUG] │ │ └─ 4 Methods added
diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md
index 2a056da4..2970a6cc 100644
--- a/docs/DEBUG_LOGGING.md
+++ b/docs/DEBUG_LOGGING.md
@@ -104,6 +104,16 @@ When debug logging is enabled, you'll see detailed output with visual separators
[INFO] [DEBUG] │ │ ├─ Analyzing method: setAge(int)
[INFO] [DEBUG] │ │ │ └─ Adding field: age (type: int)
[INFO] [DEBUG] │ └─ Processed 2 possible setters: added 2 fields, skipped 0
+[INFO] [DEBUG] Processing element: OrderDto
+[INFO] [DEBUG] ├─ Extracting builder definition from: org.example.OrderDto
+[INFO] [DEBUG] │ └─ Builder will be generated as: org.example.OrderDtoBuilder
+[INFO] [DEBUG] │ └─ Processed 1 possible setters: added 1 fields, skipped 0
+[INFO] [DEBUG] Processing element: CustomerDto (with conflicts)
+[INFO] [DEBUG] ├─ Extracting builder definition from: org.example.CustomerDto
+[INFO] [DEBUG] │ └─ Builder will be generated as: org.example.CustomerDtoBuilder
+[INFO] [DEBUG] │ └─ Processed 2 possible setters: added 2 fields, skipped 0
+[WARNING] │ │ └─ Builder field conflict: field 'name' (type Optional) renamed to 'nameOptional' to avoid conflict
+[INFO] [DEBUG] Generating targets for: PersonDto
[INFO] [DEBUG] ├─ Code generation for builder: PersonDtoBuilder
[INFO] [DEBUG] │ ├─ Class builder created
[INFO] [DEBUG] │ ├─ Generating 0 constructor fields and 2 setter fields
@@ -113,17 +123,10 @@ When debug logging is enabled, you'll see detailed output with visual separators
[INFO] [DEBUG] │ ├─ Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'.
[INFO] [DEBUG] │ ├─ Writing builder class to file: org.example.PersonDtoBuilder
[INFO] [DEBUG] │ └─ Successfully generated builder: PersonDtoBuilder
-[INFO] [DEBUG] Processing element: OrderDto
-[INFO] [DEBUG] ├─ Extracting builder definition from: org.example.OrderDto
-[INFO] [DEBUG] │ └─ Builder will be generated as: org.example.OrderDtoBuilder
-[INFO] [DEBUG] │ └─ Processed 1 possible setters: added 1 fields, skipped 0
+[INFO] [DEBUG] Generating targets for: OrderDto
[INFO] [DEBUG] ├─ Code generation for builder: OrderDtoBuilder
[INFO] [DEBUG] │ └─ Successfully generated builder: OrderDtoBuilder
-[INFO] [DEBUG] Processing element: CustomerDto (with conflicts)
-[INFO] [DEBUG] ├─ Extracting builder definition from: org.example.CustomerDto
-[INFO] [DEBUG] │ └─ Builder will be generated as: org.example.CustomerDtoBuilder
-[INFO] [DEBUG] │ └─ Processed 2 possible setters: added 2 fields, skipped 0
-[WARNING] │ │ └─ Builder field conflict: field 'name' (type Optional) renamed to 'nameOptional' to avoid conflict
+[INFO] [DEBUG] Generating targets for: CustomerDto
[INFO] [DEBUG] ├─ Code generation for builder: CustomerDtoBuilder
[INFO] [DEBUG] │ └─ Successfully generated builder: CustomerDtoBuilder
[INFO] simple-builders: Successfully generated 3 builder(s) in this processing round
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
index 77f97a0e..2c806e77 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
@@ -142,6 +142,7 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() {
"[DEBUG] │ ├─ Builder will be generated as: VerboseTestBuilder",
"[DEBUG] │ └─ Builder definition extracted: VerboseTestBuilder",
// Round 1 — code generation
+ "[DEBUG] Generating targets for: VerboseTest",
"[DEBUG] ├─ Code generation for class: VerboseTestBuilder",
"[DEBUG] │ ├─ JavaClassSource created",
"[DEBUG] │ ├─ Class metadata added",
From 84a578f8ca2dd5f6dc7a6d890c75cee741ad2397 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:47:00 +0000
Subject: [PATCH 044/170] =?UTF-8?q?processing:=20revert=20SPI=20split=20?=
=?UTF-8?q?=E2=80=94=20the=20lifecycle=20holder=20is=20builder=20domain=20?=
=?UTF-8?q?specific?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/ProcessorSpiIntegration.java | 113 ------------------
.../SimpleBuildersSpiIntegration.java | 74 +++++++++---
.../SimpleBuildersSpiIntegrationTest.java | 2 +-
3 files changed, 59 insertions(+), 130 deletions(-)
delete mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/ProcessorSpiIntegration.java
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorSpiIntegration.java b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorSpiIntegration.java
deleted file mode 100644
index d412b6df..00000000
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorSpiIntegration.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025-2026 Andreas Igel
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package org.javahelpers.codegen.processing;
-
-import java.util.concurrent.atomic.AtomicReference;
-import javax.lang.model.util.Elements;
-
-/**
- * The lifecycle an SPI integration shares between a processor and the SPI adapters of other
- * frameworks on the same annotation-processor path (and therefore the same classloader).
- *
- *
One instance is owned by the processor's domain holder (usually a static field). The processor
- * transitions it alone — SPI adapters only ever read it, they never initialize or mutate it. All
- * state is compilation-scoped: javac initializes each processor lazily when its turn in a round
- * comes, so before the processor has run its {@code init()} nothing can be trusted — values may be
- * leftovers of a previous compilation in a long-lived JVM (Gradle daemon, incremental builds).
- * Adapters therefore pass their own {@link Elements} instance to every read: javac hands every
- * processor of one compilation the same {@code Elements} instance, so a mismatch means this holder
- * still describes an older run.
- */
-public final class ProcessorSpiIntegration {
-
- /**
- * Where the owning processor's generation stands in the current compilation, as SPI adapters
- * observe it.
- */
- public enum State {
- /**
- * No processor init happened in this compilation yet — published content may be a previous
- * run's, and the registry must be treated as possibly still filling.
- */
- INIT,
- /**
- * The processor initialized: its generating round may not have run yet — the registry must be
- * treated as possibly still filling.
- */
- PROCESSING,
- /** The generating round ran: the registry is final for this compilation. */
- FINISHED
- }
-
- private volatile State state = State.INIT;
-
- /** The {@link Elements} of the compilation this holder's content describes. */
- private final AtomicReference compilationElements = new AtomicReference<>();
-
- /** Starts a new compilation: marks generation as running for the given compilation. */
- public void initCompilation(Elements elements) {
- compilationElements.set(elements);
- state = State.PROCESSING;
- }
-
- /** Marks generation final: the generating round ran (or the compilation ended without one). */
- public void finishCompilation() {
- state = State.FINISHED;
- }
-
- /**
- * Whether {@code observed} belongs to the compilation this holder currently describes — {@code
- * false} while it still carries a previous run's leftovers. javac creates one {@link Elements}
- * per compilation and shares it between all processors, so reference identity is a reliable
- * staleness check that never requires the SPI adapters to write anything.
- */
- public boolean isCurrentCompilation(Elements observed) {
- return observed != null && observed == compilationElements.get();
- }
-
- /** The lifecycle state of the current compilation. */
- public State state() {
- return state;
- }
-
- /**
- * Whether the owning processor finished generating for the compilation {@code observed} belongs
- * to — {@code false} while this holder still describes another run.
- *
- * The guard is not optional: an SPI holder's statics live in the processor-path classloader,
- * which build tools reuse across compilations (Gradle daemon, in-process javac, IDE builds,
- * repeated compiles in one JVM). In a new compilation's first round — before the owning
- * processor's {@code init()} had its turn — {@code state} can still be the previous run's {@link
- * State#FINISHED} and the registry still holds its entries. Trusting them would claim outputs by
- * name they were never planned with here and answer misses as final instead of deferring. javac
- * hands every processor of one compilation the same {@link Elements} instance and a different one
- * per compilation, so identity is the read-only, order-independent boundary marker — an SPI-side
- * reset cannot work, because SPI init order against the owning processor's init is path-order
- * dependent.
- */
- public boolean isFinishedForIntegration(Elements observed) {
- return isCurrentCompilation(observed) && state == State.FINISHED;
- }
-}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegration.java
index 2e1da684..adc360d8 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegration.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegration.java
@@ -27,9 +27,8 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
import javax.lang.model.util.Elements;
-import org.javahelpers.codegen.processing.ProcessorSpiIntegration;
-import org.javahelpers.codegen.processing.ProcessorSpiIntegration.State;
import org.javahelpers.simple.builders.processor.model.type.ResolvedBuilder;
import org.javahelpers.simple.builders.processor.model.type.TypeInstantiation.StaticFactoryCall;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
@@ -40,14 +39,39 @@
* resolved builder's type, creation and build method names plus the bean's configured {@code
* setterPrefix}, so adapters do not scan candidates or read annotation configuration themselves.
*
- *
The lifecycle — where {@link BuilderProcessor}'s builder generation stands — is delegated to a
- * {@link ProcessorSpiIntegration} instance and transitioned by the processor alone: SPI adapters
- * only ever read this holder, they never initialize or mutate it.
+ *
The lifecycle reports where {@link BuilderProcessor}'s builder generation stands and is
+ * transitioned by that processor alone — SPI adapters only ever read this holder, they never
+ * initialize or mutate it. All state is compilation-scoped: javac initializes each processor lazily
+ * when its turn in a round comes, so before {@link BuilderProcessor} has run its {@code init()}
+ * nothing static can be trusted — values may be leftovers of a previous compilation in a long-lived
+ * JVM (Gradle daemon, incremental builds). Adapters therefore pass their own {@link Elements}
+ * instance to every read: javac hands every processor of one compilation the same {@code Elements}
+ * instance, so a mismatch means this holder still describes an older run.
*/
public final class SimpleBuildersSpiIntegration {
- /** The processor-lifecycle holder this domain's SPI integration delegates to. */
- private static final ProcessorSpiIntegration LIFECYCLE = new ProcessorSpiIntegration();
+ /**
+ * Where {@link BuilderProcessor}'s builder generation stands in the current compilation, as the
+ * SPI adapters observe it.
+ */
+ public enum State {
+ /**
+ * No processor init happened in this compilation yet — static content may be a previous run's,
+ * and the registry must be treated as possibly still filling.
+ */
+ INIT,
+ /**
+ * {@code BuilderProcessor} initialized: its single generating round may not have run yet — the
+ * registry must be treated as possibly still filling.
+ */
+ PROCESSING,
+ /**
+ * The generating round ran: the registry is final for this compilation. {@code
+ * BuilderProcessor} generates in exactly one round — the first round carrying its annotations —
+ * so elements first appearing in later rounds get no builder.
+ */
+ FINISHED
+ }
/**
* One bean-to-builder pair resolved at registration time: the type this processor emits for
@@ -61,6 +85,11 @@ public record PublishedBuilder(
String buildMethodName,
String setterPrefix) {}
+ private static volatile State state = State.INIT;
+
+ /** The {@link Elements} of the compilation this holder's content describes. */
+ private static final AtomicReference compilationElements = new AtomicReference<>();
+
/** Builders published for the current compilation, keyed by bean qualified name. */
private static final Map BY_BEAN = new HashMap<>();
@@ -73,7 +102,8 @@ private SimpleBuildersSpiIntegration() {}
static void initCompilation(Elements elements) {
BY_BEAN.clear();
BY_BUILDER.clear();
- LIFECYCLE.initCompilation(elements);
+ compilationElements.set(elements);
+ state = State.PROCESSING;
}
/**
@@ -81,29 +111,41 @@ static void initCompilation(Elements elements) {
* compilation ended without one) — no more builders will be published.
*/
static void finishCompilation() {
- LIFECYCLE.finishCompilation();
+ state = State.FINISHED;
}
/**
- * Whether {@code observed} belongs to the compilation this holder currently describes — see
- * {@link ProcessorSpiIntegration#isCurrentCompilation}.
+ * Whether {@code observed} belongs to the compilation this holder currently describes — {@code
+ * false} while it still carries a previous run's leftovers. javac creates one {@link Elements}
+ * per compilation and shares it between all processors, so reference identity is a reliable
+ * staleness check that never requires the SPI adapters to write anything.
*/
static boolean isCurrentCompilation(Elements observed) {
- return LIFECYCLE.isCurrentCompilation(observed);
+ return observed != null && observed == compilationElements.get();
}
/** The lifecycle state of the current compilation. */
static State state() {
- return LIFECYCLE.state();
+ return state;
}
/**
* Whether {@link BuilderProcessor} finished generating builders for the compilation {@code
- * observed} belongs to — {@code false} while this holder still describes another run. See {@link
- * ProcessorSpiIntegration#isFinishedForIntegration} for the staleness semantics.
+ * observed} belongs to — {@code false} while this holder still describes another run.
+ *
+ * The guard is not optional: this holder's statics live in the processor-path classloader,
+ * which build tools reuse across compilations (Gradle daemon, in-process javac, IDE builds,
+ * repeated compiles in one JVM). In a new compilation's first round — before {@link
+ * BuilderProcessor}'s {@code init()} had its turn — {@code state} can still be the previous run's
+ * {@link State#FINISHED} and the registry still holds its entries. Trusting them would claim
+ * beans by qualified name they were never planned with here and answer misses as final instead of
+ * deferring. javac hands every processor of one compilation the same {@link Elements} instance
+ * and a different one per compilation, so identity is the read-only, order-independent boundary
+ * marker — an SPI-side reset cannot work, because SPI init order against {@link
+ * BuilderProcessor}'s init is path-order dependent.
*/
public static boolean isSimpleBuildersFinishedForIntegration(Elements observed) {
- return LIFECYCLE.isFinishedForIntegration(observed);
+ return isCurrentCompilation(observed) && state == State.FINISHED;
}
/**
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegrationTest.java
index 11bee26c..47b772b9 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegrationTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuildersSpiIntegrationTest.java
@@ -31,8 +31,8 @@
import java.lang.reflect.Proxy;
import java.util.Optional;
import javax.lang.model.util.Elements;
-import org.javahelpers.codegen.processing.ProcessorSpiIntegration.State;
import org.javahelpers.simple.builders.processor.SimpleBuildersSpiIntegration.PublishedBuilder;
+import org.javahelpers.simple.builders.processor.SimpleBuildersSpiIntegration.State;
import org.javahelpers.simple.builders.processor.model.type.ResolvedBuilder;
import org.javahelpers.simple.builders.processor.model.type.TypeInstantiation.StaticFactoryCall;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
From b9c3c8d4fc7b4a614d9155be029cd767f6181c2f Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:47:26 +0000
Subject: [PATCH 045/170] =?UTF-8?q?analysis:=20findAnnotationAttributeArra?=
=?UTF-8?q?y=20=E2=80=94=20name=20the=20array-shaped=20result?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../simple/builders/processor/BuilderProcessor.java | 4 ++--
.../simple/builders/processor/analysis/JavaLangAnalyser.java | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index c2a29930..9009e1dc 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -357,7 +357,7 @@ private List extractExternalTargetTypes(
Map targets = new LinkedHashMap<>();
// For an array-valued attribute javac always delivers a list, even for a single entry.
for (Object item :
- JavaLangAnalyser.findAnnotationAttributeValues(annotationInstance, "value", context)) {
+ JavaLangAnalyser.findAnnotationAttributeArray(annotationInstance, "value", context)) {
TypeElement target = resolveExternalTargetType(holder, item);
targets.put(target.getQualifiedName().toString(), target);
}
@@ -374,7 +374,7 @@ private List extractExternalTargetTypes(
* instance.
*/
private List readSourcePackageNames(AnnotationMirror annotationInstance) {
- return JavaLangAnalyser.findAnnotationAttributeValues(
+ return JavaLangAnalyser.findAnnotationAttributeArray(
annotationInstance, "sourcePackages", context)
.stream()
.map(item -> item instanceof AnnotationValue value ? value.getValue() : null)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java
index 102e598e..f6abc825 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java
@@ -322,7 +322,7 @@ public static Optional findAnnotationAttribute(
* @param context the processing context providing element utilities
* @return the attribute's raw items, or an empty list when absent or not array-valued
*/
- public static List> findAnnotationAttributeValues(
+ public static List> findAnnotationAttributeArray(
AnnotationMirror annotationMirror,
String attributeName,
AnnotationProcessingContext context) {
From 6721ada7d7af6b782f6b6038f2ba0c4bdd84c8f0 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:48:05 +0000
Subject: [PATCH 046/170] =?UTF-8?q?processing:=20analyse=20yields=20a=20de?=
=?UTF-8?q?finition=20list=20=E2=80=94=20one=20task=20may=20spawn=20severa?=
=?UTF-8?q?l=20specifications?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 33 ++++++++++---------
.../builders/processor/BuilderProcessor.java | 4 +--
2 files changed, 20 insertions(+), 17 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 560248aa..5c127283 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -64,8 +64,8 @@
* tasks = planTasks(elements, ctx) [implement]
* registerPlannedOutputs(tasks, ctx) [framework; per-task names from registerPlannedGenerationTargets() — implement]
* loop 1 — specification (guarded, strict-mode reporting): [framework]
- * def = analyse(task, ctx) [implement]
- * targets = mapToTargets(def, task, ctx) [implement]
+ * defs = analyse(task, ctx) [implement]
+ * targets = mapToTargets(def, task, ctx) (per def) [implement]
* loop 2 — generation: [framework]
* for each target:
* existsGuard → engine.generate(target, formatterResolver.resolve(target.mode)) [framework]
@@ -182,20 +182,22 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
context.initProcessingTarget(
new ProcessingTarget<>(task.configuration(), task.outputPackage()));
tracker.startPhase();
- D definition;
+ List definitions;
try {
- definition = analyse(task, context);
+ definitions = analyse(task, context);
} finally {
tracker.endPhase(analysisPhase());
}
- tracker.startPhase();
- List targets;
- try {
- targets = mapToTargets(definition, task, context);
- } finally {
- tracker.endPhase(mappingPhase());
+ for (D definition : definitions) {
+ tracker.startPhase();
+ List targets;
+ try {
+ targets = mapToTargets(definition, task, context);
+ } finally {
+ tracker.endPhase(mappingPhase());
+ }
+ planned.add(new PlannedWork<>(task, definition, targets));
}
- planned.add(new PlannedWork<>(task, definition, targets));
} catch (Exception ex) {
// By default generation failures are warnings so other targets are still generated.
// In opt-in strict mode they are promoted to errors that fail the build.
@@ -603,15 +605,16 @@ protected String mappingPhase() {
}
/**
- * Analyses one planned task into its domain definition — the analysis half of the
- * analysis/generation separation.
+ * Analyses one planned task into its domain definitions — the analysis half of the
+ * analysis/generation separation. A task may resolve to several definitions (e.g. a repeatable
+ * annotation on one element).
*
* @param task the planned task
* @param context the processing context
- * @return the domain definition
+ * @return the domain definitions, possibly empty
* @throws Exception when analysis fails; reported as a per-element failure
*/
- protected abstract D analyse(CodegenTask task, C context) throws Exception;
+ protected abstract List analyse(CodegenTask task, C context) throws Exception;
/**
* Maps a domain definition to render targets — the generation half of the analysis/generation
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 9009e1dc..5734cede 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -481,10 +481,10 @@ protected String mappingPhase() {
}
@Override
- protected BuilderDefinitionDto analyse(
+ protected List analyse(
CodegenTask task, BuilderProcessingContext ctx)
throws BuilderException {
- return extractFromElement(task.element(), ctx);
+ return List.of(extractFromElement(task.element(), ctx));
}
@Override
From 806cb60fe887ad7a69981840fe274b534dbf3e94 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:48:56 +0000
Subject: [PATCH 047/170] =?UTF-8?q?processing:=20one=20definition=20maps?=
=?UTF-8?q?=20to=20exactly=20one=20target=20=E2=80=94=20mapToTargetSpecifi?=
=?UTF-8?q?cation=20takes=20the=20configuration?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 49 ++++++++++---------
.../builders/processor/BuilderProcessor.java | 11 ++---
2 files changed, 30 insertions(+), 30 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 5c127283..1144c0c5 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -53,8 +53,8 @@
* A new processor declares its static surface once as {@link CodegenProcessorConfiguration}
* (passed to the constructor — element spec, options, source level) and implements only the domain
* hooks marked {@code [implement]}: {@link #createContext}, {@link #planTasks}, {@link
- * #registerPlannedGenerationTargets}, {@link #analyse} and {@link #mapToTargets}. Every other step
- * is framework-owned or an optional {@code [default]} the domain may override.
+ * #registerPlannedGenerationTargets}, {@link #analyse} and {@link #mapToTargetSpecification}. Every
+ * other step is framework-owned or an optional {@code [default]} the domain may override.
*
*
The round spine:
*
@@ -65,11 +65,11 @@
* registerPlannedOutputs(tasks, ctx) [framework; per-task names from registerPlannedGenerationTargets() — implement]
* loop 1 — specification (guarded, strict-mode reporting): [framework]
* defs = analyse(task, ctx) [implement]
- * targets = mapToTargets(def, task, ctx) (per def) [implement]
+ * target = mapToTargetSpecification(def, cfg, ctx) (per def) [implement]
* loop 2 — generation: [framework]
* for each target:
* existsGuard → engine.generate(target, formatterResolver.resolve(target.mode)) [framework]
- * onTaskGenerated(def, targets, task, ctx) [default: no-op]
+ * onTaskGenerated(def, target, task, ctx) [default: no-op]
* endRound(ctx) [default: no-op]
* last round: onProcessingOver(ctx) [default: drains context.getDeferredTargets()]
*
@@ -97,7 +97,9 @@ public abstract class AbstractCodegenProcessor<
/** Default performance phase for {@link #analyse} — domains may name their own. */
private static final String PHASE_ANALYSIS = "Analysis";
- /** Default performance phase for {@link #mapToTargets} — domains may name their own. */
+ /**
+ * Default performance phase for {@link #mapToTargetSpecification} — domains may name their own.
+ */
private static final String PHASE_TARGET_MAPPING = "Target Mapping";
/** The domain's context instance. */
@@ -190,13 +192,13 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
}
for (D definition : definitions) {
tracker.startPhase();
- List targets;
+ GenerationTargetClassDto target;
try {
- targets = mapToTargets(definition, task, context);
+ target = mapToTargetSpecification(definition, task.configuration(), context);
} finally {
tracker.endPhase(mappingPhase());
}
- planned.add(new PlannedWork<>(task, definition, targets));
+ planned.add(new PlannedWork<>(task, definition, target));
}
} catch (Exception ex) {
// By default generation failures are warnings so other targets are still generated.
@@ -221,11 +223,9 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
context.initProcessingTarget(
new ProcessingTarget<>(work.task().configuration(), work.task().outputPackage()));
tracker.startPhase();
- for (GenerationTargetClassDto target : work.targets()) {
- generateTarget(target);
- }
+ generateTarget(work.target());
tracker.endPhase(PHASE_CODE_GENERATION);
- onTaskGenerated(work.definition(), work.targets(), work.task(), context);
+ onTaskGenerated(work.definition(), work.target(), work.task(), context);
successfulGenerations++;
} catch (Exception ex) {
context.reportBasedOnStrictMode(
@@ -597,8 +597,8 @@ protected String analysisPhase() {
}
/**
- * The performance phase {@link #mapToTargets} is reported under — domain-named. Default {@value
- * #PHASE_TARGET_MAPPING}.
+ * The performance phase {@link #mapToTargetSpecification} is reported under — domain-named.
+ * Default {@value #PHASE_TARGET_MAPPING}.
*/
protected String mappingPhase() {
return PHASE_TARGET_MAPPING;
@@ -617,29 +617,30 @@ protected String mappingPhase() {
protected abstract List analyse(CodegenTask task, C context) throws Exception;
/**
- * Maps a domain definition to render targets — the generation half of the analysis/generation
- * separation, separated by the {@link GenerationTargetClassDto} boundary.
+ * Maps a domain definition to its render target — the generation half of the analysis/generation
+ * separation, separated by the {@link GenerationTargetClassDto} boundary. One definition
+ * specifies exactly one target.
*
* @param definition the analysed definition
- * @param task the planned task
+ * @param configuration the configuration the task resolved
* @param context the processing context
- * @return the class definitions to generate (usually one)
+ * @return the class definition to generate
* @throws Exception when mapping fails; reported as a per-element failure
*/
- protected abstract List mapToTargets(
- D definition, CodegenTask task, C context) throws Exception;
+ protected abstract GenerationTargetClassDto mapToTargetSpecification(
+ D definition, K configuration, C context) throws Exception;
/**
- * Event after all targets of a task generated successfully — the insert point for deferred-output
+ * Event after a task's target generated successfully — the insert point for deferred-output
* accumulation, per-class metrics, and domain bookkeeping. Default: nothing.
*
* @param definition the analysed definition
- * @param targets the generated class definitions
+ * @param target the generated class definition
* @param task the planned task
* @param context the processing context
*/
protected void onTaskGenerated(
- D definition, List targets, CodegenTask task, C context) {}
+ D definition, GenerationTargetClassDto target, CodegenTask task, C context) {}
/**
* Callback for the last processing round — generates every {@link DeferredTarget} the domain
@@ -672,5 +673,5 @@ protected void endRound(C context) {}
/** One task's specification output, carried from the analysis loop into the generation loop. */
private record PlannedWork(
- CodegenTask task, D definition, List targets) {}
+ CodegenTask task, D definition, GenerationTargetClassDto target) {}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 5734cede..84d0f3d0 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -488,21 +488,20 @@ protected List analyse(
}
@Override
- protected List mapToTargets(
+ protected GenerationTargetClassDto mapToTargetSpecification(
BuilderDefinitionDto definition,
- CodegenTask task,
+ BuilderConfiguration configuration,
BuilderProcessingContext ctx) {
- return List.of(
- new BuilderToGenerationTypeMapper(task.configuration()).toRenderingDto(definition));
+ return new BuilderToGenerationTypeMapper(configuration).toRenderingDto(definition);
}
@Override
protected void onTaskGenerated(
BuilderDefinitionDto definition,
- List targets,
+ GenerationTargetClassDto target,
CodegenTask task,
BuilderProcessingContext ctx) {
- GenerationTargetClassDto renderingDto = targets.get(0);
+ GenerationTargetClassDto renderingDto = target;
int collectionCount =
(int)
definition.getAllFieldsForBuilder().stream()
From 8376aa1b46e15516ae6afdeef6e8a2c60cbfda32 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:54:04 +0000
Subject: [PATCH 048/170] =?UTF-8?q?processing:=20supported=20types=20come?=
=?UTF-8?q?=20from=20the=20annotation,=20not=20the=20spec=20=E2=80=94=20sp?=
=?UTF-8?q?ec's=20types=20are=20for=20filtering?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 31 ++++---------------
.../processing/AnnotationElementSpec.java | 26 ----------------
2 files changed, 6 insertions(+), 51 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 1144c0c5..47912e74 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -159,7 +159,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
}
if (roundEnv.processingOver()) {
onProcessingOver(context);
- return claimsAnnotations(supportedAnnotationTypes());
+ return claimsAnnotations(getSupportedAnnotationTypes());
}
PerformanceTracker tracker = context.getPerformanceTracker();
@@ -172,7 +172,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
List> tasks = plan(collected, context);
if (tasks.isEmpty()) {
- return claimsAnnotations(supportedAnnotationTypes());
+ return claimsAnnotations(getSupportedAnnotationTypes());
}
// First loop: specification — analyse every task and map its render targets.
@@ -248,7 +248,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
// Reset indentation level at the end of each processing round to prevent cascading errors
context.resetIndentation();
endRound(context);
- return claimsAnnotations(supportedAnnotationTypes());
+ return claimsAnnotations(getSupportedAnnotationTypes());
}
@Override
@@ -260,26 +260,13 @@ public final Set getSupportedOptions() {
@Override
public final Set getSupportedAnnotationTypes() {
- return supportedAnnotationTypes();
- }
-
- /**
- * The annotation type names this processor supports. Default: the {@link
- * SupportedAnnotationTypes} annotation on the processor class when present, else the concrete
- * annotation types of the configured {@link CodegenProcessorConfiguration#elementSpec} — or
- * {@code "*"} when a selector uses meta-annotation discovery (its types cannot be enumerated).
- * Domains either annotate their processor class, declare concrete types in the element spec or
- * override this function.
- *
- * @return the supported annotation type names
- */
- protected Set supportedAnnotationTypes() {
- return supportedAnnotationTypesByAnnotation().orElseGet(this::supportedAnnotationTypesBySpec);
+ return supportedAnnotationTypesByAnnotation().orElse(Set.of("*"));
}
/**
* The supported annotation type names declared by the {@link SupportedAnnotationTypes} annotation
- * on the processor class.
+ * on the processor class. The element spec's annotation types are for collection filtering — not
+ * the supported-set declaration — so without the annotation the supported set stays {@code "*"}.
*
* @return the declared names, or empty when the class is not annotated
*/
@@ -290,12 +277,6 @@ protected Optional> supportedAnnotationTypesByAnnotation() {
: Optional.empty();
}
- private Set supportedAnnotationTypesBySpec() {
- return elementSpec().hasMetaMarkedSelector()
- ? Set.of("*")
- : elementSpec().annotationTypeNames();
- }
-
/**
* Additional supported option names beyond the declared registry — for wrapper or embedder
* processors adding their own options without redeclaring the domain's set (the seam MapStruct
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
index 0913465b..7326b413 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
@@ -28,7 +28,6 @@
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -93,31 +92,6 @@ public AnnotationElementSpec build() {
}
}
- /**
- * Whether any selector uses meta-annotation discovery. Then the supported annotation types cannot
- * be enumerated — user-defined annotation types match the marker without being named.
- *
- * @return {@code true} when a selector has a meta marker
- */
- public boolean hasMetaMarkedSelector() {
- return selectors.stream().anyMatch(selector -> selector.metaMarker() != null);
- }
-
- /**
- * The concrete annotation type names declared across all selectors.
- *
- * @return the canonical annotation type names the spec selects directly
- */
- public Set annotationTypeNames() {
- Set names = new LinkedHashSet<>();
- for (ElementSelector selector : selectors) {
- for (Class extends java.lang.annotation.Annotation> type : selector.annotationTypes()) {
- names.add(type.getCanonicalName());
- }
- }
- return names;
- }
-
/**
* Collects the elements selected by every selector in the given round.
*
From 79d16a2e4723c32e5c4862f71afc935c63069cf3 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:54:15 +0000
Subject: [PATCH 049/170] processing: unsupported-JDK reporting extracted into
a helper
---
.../processing/AbstractCodegenProcessor.java | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 47912e74..7e863faa 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -137,13 +137,18 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
SourceVersion current = processingEnv.getSourceVersion();
this.supportedJdk = isAtLeast(current, requiredSourceVersion());
if (!this.supportedJdk) {
- String releaseNumber = requiredSourceVersion().replace("RELEASE_", "");
- context.error(
- "%s requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor.",
- processorTag(), releaseNumber, current, releaseNumber);
+ reportUnsupportedJdk(current);
}
}
+ /** Reports that the running JDK cannot meet the required source level. */
+ private void reportUnsupportedJdk(SourceVersion current) {
+ String releaseNumber = requiredSourceVersion().replace("RELEASE_", "");
+ context.error(
+ "%s requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor.",
+ processorTag(), releaseNumber, current, releaseNumber);
+ }
+
/**
* Runs one processing round — see the class javadoc for the spine. Claims the supported
* annotations only when {@link #claimsAnnotations(Set)} says so — under the default {@code "*"}
From d51abcc8b2adc9817bacfd4296a3425722a32c9d Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:54:27 +0000
Subject: [PATCH 050/170] processing: log when the required source-version name
is unknown to the JDK
---
.../codegen/processing/AbstractCodegenProcessor.java | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 7e863faa..b09ae738 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -444,10 +444,13 @@ private String requiredSourceVersion() {
* whose {@link SourceVersion} lacks the constant (e.g. a processor built on a newer JDK running
* under an older one) reports the graceful error path instead of a linkage failure.
*/
- private static boolean isAtLeast(SourceVersion current, String requiredName) {
+ private boolean isAtLeast(SourceVersion current, String requiredName) {
try {
return current.ordinal() >= SourceVersion.valueOf(requiredName).ordinal();
} catch (IllegalArgumentException ex) {
+ logger.debug(
+ "Required source version name '%s' is unknown to this JDK — treating as unsupported",
+ requiredName);
return false;
}
}
From 1d32eb290abd09a7d4df0896b32db77e2a1c5986 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:55:03 +0000
Subject: [PATCH 051/170] processing: phase names are framework literals at
their single use sites
---
.../processing/AbstractCodegenProcessor.java | 26 +++----------------
.../builders/processor/BuilderProcessor.java | 12 ---------
.../processor/processing/PipelinePhases.java | 8 +++---
.../logging/ActivePerformanceTrackerTest.java | 8 +++---
4 files changed, 12 insertions(+), 42 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index b09ae738..2361e16f 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -89,10 +89,8 @@ public abstract class AbstractCodegenProcessor<
extends AbstractProcessor {
/** Performance phase the element collection is reported under — framework-owned. */
- private static final String PHASE_ELEMENT_COLLECTION = "Element Collection";
/** Performance phase target generation is reported under — framework-owned. */
- private static final String PHASE_CODE_GENERATION = "Code Generation";
/** Default performance phase for {@link #analyse} — domains may name their own. */
private static final String PHASE_ANALYSIS = "Analysis";
@@ -172,7 +170,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
tracker.startPhase();
ElementCollection collected = findElements(annotations, roundEnv, context);
- tracker.endPhase(PHASE_ELEMENT_COLLECTION);
+ tracker.endPhase("Element Collection");
context.debug("%s: Processing round started.", processorTag());
List> tasks = plan(collected, context);
@@ -193,7 +191,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
try {
definitions = analyse(task, context);
} finally {
- tracker.endPhase(analysisPhase());
+ tracker.endPhase("Analysis");
}
for (D definition : definitions) {
tracker.startPhase();
@@ -201,7 +199,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
try {
target = mapToTargetSpecification(definition, task.configuration(), context);
} finally {
- tracker.endPhase(mappingPhase());
+ tracker.endPhase("Target Mapping");
}
planned.add(new PlannedWork<>(task, definition, target));
}
@@ -229,7 +227,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
new ProcessingTarget<>(work.task().configuration(), work.task().outputPackage()));
tracker.startPhase();
generateTarget(work.target());
- tracker.endPhase(PHASE_CODE_GENERATION);
+ tracker.endPhase("Code Generation");
onTaskGenerated(work.definition(), work.target(), work.task(), context);
successfulGenerations++;
} catch (Exception ex) {
@@ -577,22 +575,6 @@ protected boolean isFinished(C context) {
return processedOnce;
}
- /**
- * The performance phase {@link #analyse} is reported under — domain-named. Default {@value
- * #PHASE_ANALYSIS}.
- */
- protected String analysisPhase() {
- return PHASE_ANALYSIS;
- }
-
- /**
- * The performance phase {@link #mapToTargetSpecification} is reported under — domain-named.
- * Default {@value #PHASE_TARGET_MAPPING}.
- */
- protected String mappingPhase() {
- return PHASE_TARGET_MAPPING;
- }
-
/**
* Analyses one planned task into its domain definitions — the analysis half of the
* analysis/generation separation. A task may resolve to several definitions (e.g. a repeatable
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 84d0f3d0..9c041d7d 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -25,9 +25,7 @@
package org.javahelpers.simple.builders.processor;
import static org.javahelpers.simple.builders.processor.processing.BuilderDefinitionCreator.extractFromElement;
-import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_BUILDER_DEFINITION_EXTRACTION;
import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_CONFIGURATION_RESOLUTION;
-import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_DTO_MAPPING;
import com.google.auto.service.AutoService;
import java.util.ArrayList;
@@ -470,16 +468,6 @@ protected void onPlannedOutputRegistered(
}
}
- @Override
- protected String analysisPhase() {
- return PHASE_BUILDER_DEFINITION_EXTRACTION;
- }
-
- @Override
- protected String mappingPhase() {
- return PHASE_DTO_MAPPING;
- }
-
@Override
protected List analyse(
CodegenTask task, BuilderProcessingContext ctx)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/PipelinePhases.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/PipelinePhases.java
index f045feea..a1095b81 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/PipelinePhases.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/PipelinePhases.java
@@ -41,8 +41,8 @@ public final class PipelinePhases {
// Top-level phases
public static final String PHASE_ELEMENT_COLLECTION = "Element Collection";
public static final String PHASE_CONFIGURATION_RESOLUTION = "Configuration Resolution";
- public static final String PHASE_BUILDER_DEFINITION_EXTRACTION = "Builder Definition Extraction";
- public static final String PHASE_DTO_MAPPING = "DTO Mapping";
+ public static final String PHASE_ANALYSIS = "Analysis";
+ public static final String PHASE_TARGET_MAPPING = "Target Mapping";
public static final String PHASE_CODE_GENERATION = "Code Generation";
/** Top-level phases of the pipeline, in report display order. */
@@ -50,8 +50,8 @@ public final class PipelinePhases {
List.of(
PHASE_ELEMENT_COLLECTION,
PHASE_CONFIGURATION_RESOLUTION,
- PHASE_BUILDER_DEFINITION_EXTRACTION,
- PHASE_DTO_MAPPING,
+ PHASE_ANALYSIS,
+ PHASE_TARGET_MAPPING,
PHASE_CODE_GENERATION);
/** Phase hierarchy for report display: parent phase to ordered child phases. */
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java
index c8ea663b..aeb47b9f 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java
@@ -53,7 +53,7 @@ private JsonNode generateReportAndParseJson(String outputFile) throws IOExceptio
new ActivePerformanceTracker(outputFile, PipelinePhases.PIPELINE_HIERARCHY);
tracker.startClass("TestClassA");
tracker.startPhase();
- tracker.endPhase(PipelinePhases.PHASE_BUILDER_DEFINITION_EXTRACTION);
+ tracker.endPhase(PipelinePhases.PHASE_ANALYSIS);
tracker.startGenerator();
tracker.endGenerator("FieldSupplierGenerator");
tracker.startEnhancer();
@@ -62,7 +62,7 @@ private JsonNode generateReportAndParseJson(String outputFile) throws IOExceptio
tracker.startClass("TestClassB");
tracker.startPhase();
- tracker.endPhase(PipelinePhases.PHASE_DTO_MAPPING);
+ tracker.endPhase(PipelinePhases.PHASE_TARGET_MAPPING);
tracker.endClass(3, 1);
CapturingProcessingLogger capturing = CapturingProcessingLogger.create();
@@ -285,8 +285,8 @@ void jsonReport_phaseBreakdown_containsPhaseHierarchy() throws IOException {
JsonNode phases = root.get("phaseBreakdown");
assertTrue(phases.has(PipelinePhases.PHASE_CONFIGURATION_RESOLUTION));
- assertTrue(phases.has(PipelinePhases.PHASE_BUILDER_DEFINITION_EXTRACTION));
- assertTrue(phases.has(PipelinePhases.PHASE_DTO_MAPPING));
+ assertTrue(phases.has(PipelinePhases.PHASE_ANALYSIS));
+ assertTrue(phases.has(PipelinePhases.PHASE_TARGET_MAPPING));
assertTrue(phases.has(PipelinePhases.PHASE_CODE_GENERATION));
JsonNode codeGen = phases.get(PipelinePhases.PHASE_CODE_GENERATION);
From 71d02dad03fe4fc42b3524add6ed4b5fb0b25941 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:55:52 +0000
Subject: [PATCH 052/170] =?UTF-8?q?processing:=20generated=20units=20are?=
=?UTF-8?q?=20targets=20=E2=80=94=20the=20domain's=20noun=20leaves=20the?=
=?UTF-8?q?=20driver?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/CONTRIBUTING.md | 2 +-
docs/DEBUG_LOGGING.md | 8 ++---
.../processing/AbstractCodegenProcessor.java | 30 +++++--------------
.../builders/processor/BuilderProcessor.java | 6 ----
.../BuilderConfigurationReaderTest.java | 4 +--
.../processor/BuilderProcessorTest.java | 6 ++--
.../RoasterCodeGeneratorEdgeCasesTest.java | 2 +-
.../processor/SimpleBuilderForTest.java | 4 +--
.../builders/processor/StrictModeTest.java | 4 +--
9 files changed, 23 insertions(+), 43 deletions(-)
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 6fddef63..0cfd6789 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -255,7 +255,7 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md).
[DEBUG] │ ├─ Adding Methods for 4 candidates
[DEBUG] │ │ └─ 4 Methods added
[DEBUG] │ └─ Successfully generated builder: ProjectBuilder
-simple-builders: Successfully generated 1 builder(s) in this processing round
+simple-builders: Successfully generated 1 target(s) in this processing round
=============================================
========== Generated Source Files ==========
diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md
index 2970a6cc..b302e0e2 100644
--- a/docs/DEBUG_LOGGING.md
+++ b/docs/DEBUG_LOGGING.md
@@ -73,12 +73,12 @@ The Simple Builders processor provides **conditional hierarchical logging** to b
### **When Debug Mode is DISABLED (Default):**
- INFO and WARNING messages appear flat without indentation
- Suitable for production systems where log noise should be minimized
-- Example: `simple-builders: Successfully generated 3 builder(s) in this processing round`
+- Example: `simple-builders: Successfully generated 3 target(s) in this processing round`
### **When Debug Mode is ENABLED (-Averbose=true):**
- INFO and WARNING messages have no extra prefix, DEBUG messages are posted on `[INFO]` chanel so they have the prefix `[INFO] [DEBUG]`
- Provides full visibility into the processing hierarchy
-- Example: `[INFO] │ └─ simple-builders: Successfully generated 3 builder(s) in this processing round`
+- Example: `[INFO] │ └─ simple-builders: Successfully generated 3 target(s) in this processing round`
- Example: `[WARNING] │ │ ├─ Builder field conflict: field 'name'...`
- Example: `[INFO] [DEBUG] │ │ │ └─ Processing method: setName`
- All message types align vertically despite different prefix lengths
@@ -129,7 +129,7 @@ When debug logging is enabled, you'll see detailed output with visual separators
[INFO] [DEBUG] Generating targets for: CustomerDto
[INFO] [DEBUG] ├─ Code generation for builder: CustomerDtoBuilder
[INFO] [DEBUG] │ └─ Successfully generated builder: CustomerDtoBuilder
-[INFO] simple-builders: Successfully generated 3 builder(s) in this processing round
+[INFO] simple-builders: Successfully generated 3 target(s) in this processing round
[INFO] simple-builders: PROCESSING ROUND START
[INFO] [DEBUG] simple-builders: Processing round started.
[INFO] [DEBUG] simple-builders: Found 0 annotated elements.
@@ -145,7 +145,7 @@ Without debug logging enabled, you only see the INFO-level messages:
```
[INFO] simple-builders: PROCESSING ROUND START
-[INFO] simple-builders: Successfully generated 3 builder(s) in this processing round
+[INFO] simple-builders: Successfully generated 3 target(s) in this processing round
```
## Troubleshooting
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 2361e16f..d58deb74 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -208,9 +208,8 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
// In opt-in strict mode they are promoted to errors that fail the build.
context.reportBasedOnStrictMode(
task.reportingElement(),
- "%s: Failed to generate %s - %s",
+ "%s: Failed to generate target - %s",
processorTag(),
- generatedKind(),
ex.getMessage());
} finally {
context.debugEndOperation();
@@ -233,9 +232,8 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
} catch (Exception ex) {
context.reportBasedOnStrictMode(
work.task().reportingElement(),
- "%s: Failed to generate %s - %s",
+ "%s: Failed to generate target - %s",
processorTag(),
- generatedKind(),
ex.getMessage());
} finally {
context.debugEndOperation();
@@ -244,8 +242,8 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
if (successfulGenerations > 0) {
context.info(
- "%s: Successfully generated %d %s(s) in this processing round",
- processorTag(), successfulGenerations, generatedKind());
+ "%s: Successfully generated %d target(s) in this processing round",
+ processorTag(), successfulGenerations);
}
// Reset indentation level at the end of each processing round to prevent cascading errors
@@ -330,17 +328,15 @@ private void assertTargetDoesNotExist(GenerationTargetClassDto target)
exists = false;
}
if (exists) {
- String kind = generatedKind();
- String capitalized = Character.toUpperCase(kind.charAt(0)) + kind.substring(1);
throw new CodeGenerationException(
"""
- %s class '%s' already exists. This may be a manually written %s or a previously generated %s.
+ Target class '%s' already exists. This may be a manually written target or a previously generated target.
To resolve this:
- 1. If you have a manual %s, consider renaming it or removing the annotation triggering generation
+ 1. If you have a manual target, consider renaming it or removing the annotation triggering generation
2. If this is from a previous compilation, clean and rebuild the project
- 3. Check that you're not trying to generate multiple %ss for the same target type
+ 3. Check that you're not trying to generate multiple targets for the same target type
"""
- .formatted(capitalized, qualifiedName, kind, kind, kind, kind));
+ .formatted(qualifiedName));
}
}
@@ -457,16 +453,6 @@ private String processorTag() {
return logger.getTag();
}
- /**
- * What one generated unit is called in diagnostics ("Failed to generate %s", "generated N %s(s)")
- * — the domain's noun lives in domain code. Default {@code "class"}.
- *
- * @return the generated unit's name
- */
- protected String generatedKind() {
- return "class";
- }
-
/**
* Finds the elements of this round the domain generates code for. Default delegates to the
* configured {@link CodegenProcessorConfiguration#elementSpec}'s collector; override for custom
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 9c041d7d..caf02be1 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -137,12 +137,6 @@ protected String loggerTag() {
return "simple-builders";
}
- /** Builders are what this processor generates — the noun diagnostics use. */
- @Override
- protected String generatedKind() {
- return "builder";
- }
-
/** The global configuration resolved from compiler arguments — needed by several hooks. */
private BuilderConfiguration globalConfig;
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
index 81f28fa5..6b844d98 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
@@ -646,7 +646,7 @@ public class PersonDto {
// Then: Builder generation fails but compilation succeeds (no invalid Java code generated)
assertThat(compilation).succeeded();
- assertThat(compilation).hadWarningContaining("Failed to generate builder");
+ assertThat(compilation).hadWarningContaining("Failed to generate target");
assertThat(compilation).hadWarningContaining("builderAccess=PRIVATE");
assertThat(compilation).hadWarningContaining("Java does not allow private top-level classes");
}
@@ -682,7 +682,7 @@ public class PersonDto {
// Then: Builder generation fails but compilation succeeds (no invalid Java code generated)
assertThat(compilation).succeeded();
- assertThat(compilation).hadWarningContaining("Failed to generate builder");
+ assertThat(compilation).hadWarningContaining("Failed to generate target");
assertThat(compilation).hadWarningContaining("methodAccess=PRIVATE");
assertThat(compilation).hadWarningContaining("makes all setter methods inaccessible");
}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
index 2c806e77..b61df4f5 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
@@ -163,7 +163,7 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() {
"[DEBUG] │ └─ Successfully generated class: VerboseTestBuilder",
"[DEBUG] ├─ Jackson module entry added",
"[DEBUG] └─ Generated builder with 1 fields and 10 methods for VerboseTestBuilder",
- "simple-builders: Successfully generated 1 builder(s) in this processing round",
+ "simple-builders: Successfully generated 1 target(s) in this processing round",
// Round 2 — no new elements
"simple-builders: PROCESSING ROUND START",
"[DEBUG] simple-builders: Processing round started.",
@@ -3592,7 +3592,7 @@ public PersonDto build() {
assertThat(compilation).succeeded();
assertThat(compilation)
.hadWarningContaining(
- "Failed to generate builder - Builder class 'test.PersonDtoBuilder' already exists.");
+ "Failed to generate target - Target class 'test.PersonDtoBuilder' already exists.");
// The processor should detect the conflict and handle it gracefully
// Either by skipping generation or by generating with a different approach
@@ -3639,7 +3639,7 @@ public class OrderDto {
assertThat(compilation).succeeded();
assertThat(compilation)
.hadWarningContaining(
- "Failed to generate builder - Unable to create builder class 'test.OrderDtoFactory': Attempt to recreate a file for type test.OrderDtoFactory.");
+ "Failed to generate target - Unable to create builder class 'test.OrderDtoFactory': Attempt to recreate a file for type test.OrderDtoFactory.");
// Verify that a builder was generated
String generatedCode = loadGeneratedSource(compilation, "OrderDtoFactory");
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java
index c1d9944d..a0775814 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java
@@ -183,7 +183,7 @@ public class PersonDtoBuilder {
assertThat(compilation).succeeded();
assertThat(compilation)
.hadWarningContaining(
- "Failed to generate builder - Builder class 'test.PersonDtoBuilder' already exists");
+ "Failed to generate target - Target class 'test.PersonDtoBuilder' already exists");
}
/**
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java
index 93a5a098..f4eae973 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/SimpleBuilderForTest.java
@@ -120,7 +120,7 @@ private Singleton() {}
.compile(unconstructable, holder("test", "Builders", "ext.Singleton"));
assertThat(compilation).succeeded();
- assertThat(compilation).hadWarningContaining("Failed to generate builder");
+ assertThat(compilation).hadWarningContaining("Failed to generate target");
assertThat(compilation).hadWarningContaining("No accessible constructor");
ProcessorAsserts.assertNoBuilderGenerated(
compilation, "Singleton", "A type without accessible constructor must not get a builder");
@@ -165,7 +165,7 @@ private Singleton() {}
.compile(unconstructable, holder("test", "Builders", "ext.Singleton"));
assertThat(compilation).failed();
- assertThat(compilation).hadErrorContaining("Failed to generate builder");
+ assertThat(compilation).hadErrorContaining("Failed to generate target");
assertThat(compilation).hadErrorContaining("No accessible constructor");
}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/StrictModeTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/StrictModeTest.java
index b7495cec..23dac4df 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/StrictModeTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/StrictModeTest.java
@@ -68,7 +68,7 @@ void defaultMode_generationFailure_isWarning_andCompilationSucceeds() {
Compilation compilation = createCompiler().compile(dto(), collidingBuilder());
assertThat(compilation).succeeded();
- assertThat(compilation).hadWarningContaining("Failed to generate builder");
+ assertThat(compilation).hadWarningContaining("Failed to generate target");
}
@Test
@@ -79,6 +79,6 @@ void strictMode_generationFailure_isError_andCompilationFails() {
.compile(dto(), collidingBuilder());
assertThat(compilation).failed();
- assertThat(compilation).hadErrorContaining("Failed to generate builder");
+ assertThat(compilation).hadErrorContaining("Failed to generate target");
}
}
From 566d361b629fe1103a9da713aae6d4cd90e4064c Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:57:07 +0000
Subject: [PATCH 053/170] processing: the tag-prefix pattern lives inside the
logger; provideLoggerPrefix is the only hook
---
.../processing/AbstractCodegenProcessor.java | 54 +++++++++----------
.../builders/processor/BuilderProcessor.java | 12 ++---
.../processing/logging/ProcessingLogger.java | 41 ++++++++++++++
3 files changed, 72 insertions(+), 35 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index d58deb74..713cf8b1 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -144,7 +144,7 @@ private void reportUnsupportedJdk(SourceVersion current) {
String releaseNumber = requiredSourceVersion().replace("RELEASE_", "");
context.error(
"%s requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor.",
- processorTag(), releaseNumber, current, releaseNumber);
+ logger.getTag(), releaseNumber, current, releaseNumber);
}
/**
@@ -166,12 +166,12 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
}
PerformanceTracker tracker = context.getPerformanceTracker();
- context.info("%s: PROCESSING ROUND START", processorTag());
+ logger.infoTagged("PROCESSING ROUND START");
tracker.startPhase();
ElementCollection collected = findElements(annotations, roundEnv, context);
tracker.endPhase("Element Collection");
- context.debug("%s: Processing round started.", processorTag());
+ logger.debugTagged("Processing round started.");
List> tasks = plan(collected, context);
if (tasks.isEmpty()) {
@@ -208,8 +208,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
// In opt-in strict mode they are promoted to errors that fail the build.
context.reportBasedOnStrictMode(
task.reportingElement(),
- "%s: Failed to generate target - %s",
- processorTag(),
+ logger.prefixed("Failed to generate target - %s"),
ex.getMessage());
} finally {
context.debugEndOperation();
@@ -232,8 +231,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
} catch (Exception ex) {
context.reportBasedOnStrictMode(
work.task().reportingElement(),
- "%s: Failed to generate target - %s",
- processorTag(),
+ logger.prefixed("Failed to generate target - %s"),
ex.getMessage());
} finally {
context.debugEndOperation();
@@ -241,9 +239,8 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
}
if (successfulGenerations > 0) {
- context.info(
- "%s: Successfully generated %d target(s) in this processing round",
- processorTag(), successfulGenerations);
+ logger.infoTagged(
+ "Successfully generated %d target(s) in this processing round", successfulGenerations);
}
// Reset indentation level at the end of each processing round to prevent cascading errors
@@ -341,26 +338,27 @@ private void assertTargetDoesNotExist(GenerationTargetClassDto target)
}
/**
- * The diagnostic tag the processor's messages are prefixed with — carried by the logger. Default:
- * the processor's simple class name; override for a shorter domain tag.
+ * The diagnostic prefix the processor's tagged messages carry — see {@link
+ * ProcessingLogger#prefixed}. Default: the processor's simple class name; override for a shorter
+ * domain prefix.
*
- * @return the tag
+ * @return the logger prefix
*/
- protected String loggerTag() {
+ protected String provideLoggerPrefix() {
return getClass().getSimpleName();
}
/**
- * Creates the logger. Default: verbose when the domain's declared verbose option requests it —
- * see {@link ProcessorOptionRegistry#isVerboseRequested}. The logger carries {@link
- * #loggerTag()}.
+ * Creates the logger: verbose when the domain's declared verbose option requests it — see {@link
+ * ProcessorOptionRegistry#isVerboseRequested} — and carrying {@link #provideLoggerPrefix()}.
*/
- protected ProcessingLogger createLogger(
+ private ProcessingLogger createLogger(
ProcessingEnvironment processingEnv, CompilerArgumentsReader reader) {
+ String loggerPrefix = provideLoggerPrefix();
ProcessingLogger logger =
optionRegistry().isVerboseRequested(reader)
- ? ProcessingLogger.createVerbose(processingEnv, loggerTag())
- : ProcessingLogger.create(processingEnv, loggerTag());
+ ? ProcessingLogger.createVerbose(processingEnv, loggerPrefix)
+ : ProcessingLogger.create(processingEnv, loggerPrefix);
logger.debug("Starting %s...", getClass().getSimpleName());
return logger;
}
@@ -449,10 +447,6 @@ private boolean isAtLeast(SourceVersion current, String requiredName) {
}
}
- private String processorTag() {
- return logger.getTag();
- }
-
/**
* Finds the elements of this round the domain generates code for. Default delegates to the
* configured {@link CodegenProcessorConfiguration#elementSpec}'s collector; override for custom
@@ -612,15 +606,17 @@ protected void onProcessingOver(C context) {
GenerationTargetClassDto target = deferred.target();
String packageName = target.getTypeName().getPackageName();
String kind = deferred.kind();
- context.info(
- "%s: Generating %s in package '%s'",
- processorTag(), Character.toUpperCase(kind.charAt(0)) + kind.substring(1), packageName);
+ logger.infoTagged(
+ "Generating %s in package '%s'",
+ Character.toUpperCase(kind.charAt(0)) + kind.substring(1), packageName);
try {
generateTarget(target);
} catch (Exception e) {
context.reportBasedOnStrictMode(
- "%s: Error generating %s for package %s: %s",
- processorTag(), kind, packageName, e.getMessage());
+ logger.prefixed("Error generating %s for package %s: %s"),
+ kind,
+ packageName,
+ e.getMessage());
}
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index caf02be1..c7ad1af0 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -131,17 +131,17 @@ public BuilderProcessor() {
super(PROCESSOR_CONFIGURATION);
}
- /** The tag prefixing diagnostics — the project name, not the class name. */
- @Override
- protected String loggerTag() {
- return "simple-builders";
- }
-
/** The global configuration resolved from compiler arguments — needed by several hooks. */
private BuilderConfiguration globalConfig;
private JacksonModuleGenerator jacksonModuleGenerator;
+ /** The project name prefixes diagnostics — not the class name. */
+ @Override
+ protected String provideLoggerPrefix() {
+ return "simple-builders";
+ }
+
@Override
protected BuilderProcessingContext createContext(
ProcessingEnvironment processingEnv,
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
index 4b705dc5..717bbc99 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
@@ -121,6 +121,47 @@ public String getTag() {
return tag;
}
+ /**
+ * Applies this logger's "{@code tag: }" prefix to a message format — the one place the tag-prefix
+ * pattern is defined. Untagged loggers return the format unchanged.
+ *
+ * @param format the message format
+ * @return the format prefixed with this logger's tag
+ */
+ public String prefixed(String format) {
+ return tag != null ? tag + ": " + format : format;
+ }
+
+ /**
+ * Logs an error message prefixed with this logger's tag.
+ *
+ * @param format the message format
+ * @param args the format arguments
+ */
+ public void errorTagged(String format, Object... args) {
+ error(prefixed(format), args);
+ }
+
+ /**
+ * Logs an info message prefixed with this logger's tag.
+ *
+ * @param format the message format
+ * @param args the format arguments
+ */
+ public void infoTagged(String format, Object... args) {
+ info(prefixed(format), args);
+ }
+
+ /**
+ * Logs a debug message prefixed with this logger's tag.
+ *
+ * @param format the message format
+ * @param args the format arguments
+ */
+ public void debugTagged(String format, Object... args) {
+ debug(prefixed(format), args);
+ }
+
/** Resets ThreadLocal state to ensure clean state between test runs. */
private void resetThreadLocalState() {
indentationLevel.set(0);
From 9f0ffe1e38568694640c184054fecb27f9b749a4 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:57:50 +0000
Subject: [PATCH 054/170] processing: deferred outputs push the target DTO
itself; the domain logs its own specifics
---
.../processing/AbstractCodegenProcessor.java | 21 ++++------
.../codegen/processing/DeferredTarget.java | 38 -------------------
.../builders/processor/BuilderProcessor.java | 6 ++-
.../AnnotationProcessingContext.java | 8 ++--
4 files changed, 15 insertions(+), 58 deletions(-)
delete mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/DeferredTarget.java
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 713cf8b1..4dcbccb7 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -594,28 +594,21 @@ protected void onTaskGenerated(
D definition, GenerationTargetClassDto target, CodegenTask task, C context) {}
/**
- * Callback for the last processing round — generates every {@link DeferredTarget} the domain
- * pushed onto the context through the same guarded path as round targets. Domain extensions push
- * their deferred outputs and call {@code super.onProcessingOver(context)} to drain, wrapped in
- * their finalization (SPI flush, reports).
+ * Callback for the last processing round — generates every target the domain pushed onto the
+ * context through {@link AnnotationProcessingContext#addDeferredTarget} via the same guarded path
+ * as round targets. Domain extensions log and push their deferred outputs, then call {@code
+ * super.onProcessingOver(context)} to drain, wrapped in their finalization (SPI flush, reports).
*
* @param context the processing context
*/
protected void onProcessingOver(C context) {
- for (DeferredTarget deferred : context.getDeferredTargets()) {
- GenerationTargetClassDto target = deferred.target();
- String packageName = target.getTypeName().getPackageName();
- String kind = deferred.kind();
- logger.infoTagged(
- "Generating %s in package '%s'",
- Character.toUpperCase(kind.charAt(0)) + kind.substring(1), packageName);
+ for (GenerationTargetClassDto target : context.getDeferredTargets()) {
try {
generateTarget(target);
} catch (Exception e) {
context.reportBasedOnStrictMode(
- logger.prefixed("Error generating %s for package %s: %s"),
- kind,
- packageName,
+ logger.prefixed("Error generating target for package %s: %s"),
+ target.getTypeName().getPackageName(),
e.getMessage());
}
}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/DeferredTarget.java b/processor/src/main/java/org/javahelpers/codegen/processing/DeferredTarget.java
deleted file mode 100644
index 5e833960..00000000
--- a/processor/src/main/java/org/javahelpers/codegen/processing/DeferredTarget.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025-2026 Andreas Igel
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package org.javahelpers.codegen.processing;
-
-import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
-
-/**
- * A class definition the domain wants generated deferred — pushed onto the processing context
- * during a round (typically from {@code onTaskGenerated} or the domain's last-round callback) and
- * generated by the driver in the last round through the same guarded path as round targets.
- *
- * @param target the class definition to generate
- * @param kind what the deferred output is called in diagnostics (a domain noun like "Jackson
- * module")
- */
-public record DeferredTarget(GenerationTargetClassDto target, String kind) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index c7ad1af0..7018bf49 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -51,7 +51,6 @@
import org.javahelpers.codegen.processing.AnnotationElementSpec;
import org.javahelpers.codegen.processing.CodegenProcessorConfiguration;
import org.javahelpers.codegen.processing.CodegenTask;
-import org.javahelpers.codegen.processing.DeferredTarget;
import org.javahelpers.codegen.processing.ElementCollection;
import org.javahelpers.codegen.processing.ProcessorOptionRegistry;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
@@ -526,7 +525,10 @@ protected void onProcessingOver(BuilderProcessingContext ctx) {
ctx.getPerformanceTracker().generateReport(logger);
for (GenerationTargetClassDto moduleDefinition :
jacksonModuleGenerator.getModuleDefinitions()) {
- ctx.addDeferredTarget(new DeferredTarget(moduleDefinition, "Jackson module"));
+ ctx.info(
+ "simple-builders: Generating Jackson module in package '%s'",
+ moduleDefinition.getTypeName().getPackageName());
+ ctx.addDeferredTarget(moduleDefinition);
}
super.onProcessingOver(ctx);
// Reset indentation after Jackson module generation as well
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
index 79337dad..fc1fa35a 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
@@ -40,10 +40,10 @@
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
-import org.javahelpers.codegen.processing.DeferredTarget;
import org.javahelpers.codegen.processing.ProcessingTarget;
import org.javahelpers.simple.builders.processor.analysis.GeneratedTypes;
import org.javahelpers.simple.builders.processor.analysis.TypeMappingContext;
+import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.logging.ActivePerformanceTracker;
import org.javahelpers.simple.builders.processor.processing.logging.NoOpPerformanceTracker;
@@ -93,7 +93,7 @@ public class AnnotationProcessingContext implements TypeMappingContext {
* Class definitions the domain pushed for deferred last-round generation — drained by the
* processing driver in the last round.
*/
- private final List deferredTargets = new ArrayList<>();
+ private final List deferredTargets = new ArrayList<>();
/**
* Creates the context with default options (strict mode enabled, performance tracking disabled).
@@ -167,7 +167,7 @@ public ProcessingTarget> getProcessingTarget() {
*
* @param target the deferred output and its diagnostic kind
*/
- public void addDeferredTarget(DeferredTarget target) {
+ public void addDeferredTarget(GenerationTargetClassDto target) {
deferredTargets.add(target);
}
@@ -176,7 +176,7 @@ public void addDeferredTarget(DeferredTarget target) {
*
* @return the mutable deferred-target list
*/
- public List getDeferredTargets() {
+ public List getDeferredTargets() {
return deferredTargets;
}
From 2a2e8ec6698fec2eb09ccd706156afcc9ec9276e Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:59:20 +0000
Subject: [PATCH 055/170] processing: the management function registers task
results, not outputs
---
.../codegen/processing/AbstractCodegenProcessor.java | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 4dcbccb7..b6ca344c 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -62,7 +62,7 @@
* ctx = createContext(env, reader, logger) [implement — in init]
* elements = findElements(annotations, roundEnv, ctx) [default: configuration's elementSpec]
* tasks = planTasks(elements, ctx) [implement]
- * registerPlannedOutputs(tasks, ctx) [framework; per-task names from registerPlannedGenerationTargets() — implement]
+ * registerPlannedTaskResults(tasks, ctx) [framework; per-task names from registerPlannedGenerationTargets() — implement]
* loop 1 — specification (guarded, strict-mode reporting): [framework]
* defs = analyse(task, ctx) [implement]
* target = mapToTargetSpecification(def, cfg, ctx) (per def) [implement]
@@ -481,7 +481,7 @@ private List> plan(ElementCollection collected, C context) {
// leaves it open for a later one carrying matching elements.
processedOnce = true;
}
- registerPlannedOutputs(tasks, context);
+ registerPlannedTaskResults(tasks, context);
return tasks;
}
@@ -505,7 +505,7 @@ private List> plan(ElementCollection collected, C context) {
* @param tasks the planned tasks
* @param context the processing context
*/
- protected void registerPlannedOutputs(List> tasks, C context) {
+ protected void registerPlannedTaskResults(List> tasks, C context) {
context.getGeneratedTypes().clear();
for (CodegenTask task : tasks) {
if (!(task.element() instanceof TypeElement targetType)) {
@@ -520,7 +520,7 @@ protected void registerPlannedOutputs(List> tasks, C context) {
}
/**
- * The names of the types this task will generate — the domain's task → output type-name
+ * The names of the types this task will generate — the domain's task → result type-name
* predictions, evaluated at registration time. Keep this pure: registrations and their side
* effects belong to {@link #onPlannedOutputRegistered}.
*
From a3e82dd67594dd1237161da660edf81ceb38c94a Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:59:25 +0000
Subject: [PATCH 056/170] processing: a task's prediction is its result type
names
---
.../processing/AbstractCodegenProcessor.java | 22 +++++++++----------
.../builders/processor/BuilderProcessor.java | 4 ++--
2 files changed, 12 insertions(+), 14 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index b6ca344c..193e07f6 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -53,8 +53,8 @@
* A new processor declares its static surface once as {@link CodegenProcessorConfiguration}
* (passed to the constructor — element spec, options, source level) and implements only the domain
* hooks marked {@code [implement]}: {@link #createContext}, {@link #planTasks}, {@link
- * #registerPlannedGenerationTargets}, {@link #analyse} and {@link #mapToTargetSpecification}. Every
- * other step is framework-owned or an optional {@code [default]} the domain may override.
+ * #plannedTaskResultTypeNames}, {@link #analyse} and {@link #mapToTargetSpecification}. Every other
+ * step is framework-owned or an optional {@code [default]} the domain may override.
*
*
The round spine:
*
@@ -62,7 +62,7 @@
* ctx = createContext(env, reader, logger) [implement — in init]
* elements = findElements(annotations, roundEnv, ctx) [default: configuration's elementSpec]
* tasks = planTasks(elements, ctx) [implement]
- * registerPlannedTaskResults(tasks, ctx) [framework; per-task names from registerPlannedGenerationTargets() — implement]
+ * registerPlannedTaskResults(tasks, ctx) [framework; per-task names from plannedTaskResultTypeNames() — implement]
* loop 1 — specification (guarded, strict-mode reporting): [framework]
* defs = analyse(task, ctx) [implement]
* target = mapToTargetSpecification(def, cfg, ctx) (per def) [implement]
@@ -498,8 +498,8 @@ private List> plan(ElementCollection collected, C context) {
/**
* Registers the types this round will generate, up front, so references to them resolve during
* analysis. Default: clears {@link AnnotationProcessingContext#getGeneratedTypes()} and registers
- * every name each task's {@link #registerPlannedGenerationTargets} predicts — a task may produce
- * several generated types. Each registration fires {@link #onPlannedOutputRegistered} for domain
+ * every name each task's {@link #plannedTaskResultTypeNames} predicts — a task may produce
+ * several generated types. Each registration fires {@link #onTaskResultRegistered} for domain
* extras (e.g. SPI publication); override this whole method only for a different shape.
*
* @param tasks the planned tasks
@@ -512,9 +512,9 @@ protected void registerPlannedTaskResults(List> tasks, C context)
continue;
}
TypeName elementName = JavaLangMapper.mapToTypeName(targetType, context);
- for (TypeName generated : registerPlannedGenerationTargets(task, context)) {
+ for (TypeName generated : plannedTaskResultTypeNames(task, context)) {
context.getGeneratedTypes().add(elementName, generated);
- onPlannedOutputRegistered(task, generated, context);
+ onTaskResultRegistered(task, generated, context);
}
}
}
@@ -522,14 +522,13 @@ protected void registerPlannedTaskResults(List> tasks, C context)
/**
* The names of the types this task will generate — the domain's task → result type-name
* predictions, evaluated at registration time. Keep this pure: registrations and their side
- * effects belong to {@link #onPlannedOutputRegistered}.
+ * effects belong to {@link #onTaskResultRegistered}.
*
* @param task the planned task
* @param context the processing context
* @return the generated type names (empty when the task registers nothing)
*/
- protected abstract List registerPlannedGenerationTargets(
- CodegenTask task, C context);
+ protected abstract List plannedTaskResultTypeNames(CodegenTask task, C context);
/**
* Runs after a predicted type name is registered for a task — the extension point for domain
@@ -539,8 +538,7 @@ protected abstract List registerPlannedGenerationTargets(
* @param generatedName the type name just registered for it
* @param context the processing context
*/
- protected void onPlannedOutputRegistered(
- CodegenTask task, TypeName generatedName, C context) {}
+ protected void onTaskResultRegistered(CodegenTask task, TypeName generatedName, C context) {}
/**
* Whether the domain's generating round already ran. Default: one-shot — the base marks the
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 7018bf49..5f99f8b9 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -439,7 +439,7 @@ private TypeName builderTypeName(
* targets.
*/
@Override
- protected List registerPlannedGenerationTargets(
+ protected List plannedTaskResultTypeNames(
CodegenTask task, BuilderProcessingContext ctx) {
if (!(task.element() instanceof TypeElement targetType)) {
return List.of();
@@ -449,7 +449,7 @@ protected List registerPlannedGenerationTargets(
/** SPI-exports the registered builder on top of the generic generated-type registration. */
@Override
- protected void onPlannedOutputRegistered(
+ protected void onTaskResultRegistered(
CodegenTask task,
TypeName generatedName,
BuilderProcessingContext ctx) {
From 38ca043a1a2d7c88e5d80252f60a5273fae6f66e Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 19:59:59 +0000
Subject: [PATCH 057/170] =?UTF-8?q?processing:=20it=20collects=20elements,?=
=?UTF-8?q?=20it=20does=20not=20specify=20them=20=E2=80=94=20ElementCollec?=
=?UTF-8?q?tor?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 12 ++++-----
.../CodegenProcessorConfiguration.java | 27 ++++++++++---------
.../codegen/processing/ElementCollection.java | 2 +-
...ElementSpec.java => ElementCollector.java} | 10 +++----
.../codegen/processing/ElementSelector.java | 2 +-
.../builders/processor/BuilderProcessor.java | 8 +++---
6 files changed, 31 insertions(+), 30 deletions(-)
rename processor/src/main/java/org/javahelpers/codegen/processing/{AnnotationElementSpec.java => ElementCollector.java} (95%)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 193e07f6..b35f663b 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -60,7 +60,7 @@
*
*
* ctx = createContext(env, reader, logger) [implement — in init]
- * elements = findElements(annotations, roundEnv, ctx) [default: configuration's elementSpec]
+ * elements = findElements(annotations, roundEnv, ctx) [default: configuration's elementCollector]
* tasks = planTasks(elements, ctx) [implement]
* registerPlannedTaskResults(tasks, ctx) [framework; per-task names from plannedTaskResultTypeNames() — implement]
* loop 1 — specification (guarded, strict-mode reporting): [framework]
@@ -407,8 +407,8 @@ protected SourceFormatterResolver createFormatterResolver(
backendRegistry.resolveFormatterFactory());
}
- private AnnotationElementSpec elementSpec() {
- return processorConfiguration.elementSpec();
+ private ElementCollector elementCollector() {
+ return processorConfiguration.elementCollector();
}
private ProcessorOptionRegistry optionRegistry() {
@@ -449,8 +449,8 @@ private boolean isAtLeast(SourceVersion current, String requiredName) {
/**
* Finds the elements of this round the domain generates code for. Default delegates to the
- * configured {@link CodegenProcessorConfiguration#elementSpec}'s collector; override for custom
- * element discovery.
+ * configured {@link CodegenProcessorConfiguration#elementCollector}; override for custom element
+ * discovery.
*
* @param annotations the annotation types requested this round
* @param roundEnv the round environment
@@ -459,7 +459,7 @@ private boolean isAtLeast(SourceVersion current, String requiredName) {
*/
protected ElementCollection findElements(
Set extends TypeElement> annotations, RoundEnvironment roundEnv, C context) {
- return elementSpec().collect(annotations, roundEnv, logger);
+ return elementCollector().collect(annotations, roundEnv, logger);
}
/**
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
index e8712075..e75ad5cf 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
@@ -30,21 +30,21 @@
* Static declarations of an {@link AbstractCodegenProcessor}: everything the framework needs to
* know up front, collected in one record the processor hands to the base constructor.
*
- * Only {@link #elementSpec} is required; all other components carry defaults a minimal processor
- * can leave unset. Use {@link #builder()} to build an instance.
+ *
Only {@link #elementCollector} is required; all other components carry defaults a minimal
+ * processor can leave unset. Use {@link #builder()} to build an instance.
*
- * @param elementSpec which elements the domain generates code for (required)
+ * @param elementCollector which elements the domain generates code for (required)
* @param optionRegistry the processor's declared compiler options; default: an empty registry
* @param requiredSourceVersion the minimum source level by name (e.g. {@code "RELEASE_17"});
* default: the running JDK's latest supported release
*/
public record CodegenProcessorConfiguration(
- AnnotationElementSpec elementSpec,
+ ElementCollector elementCollector,
ProcessorOptionRegistry optionRegistry,
String requiredSourceVersion) {
/**
- * Creates a builder with all defaults applied — only {@link #elementSpec(AnnotationElementSpec)}
+ * Creates a builder with all defaults applied — only {@link #elementCollector(ElementCollector)}
* must be set.
*
* @return a new builder
@@ -55,7 +55,7 @@ public static Builder builder() {
/** Fluent builder for {@link CodegenProcessorConfiguration}. */
public static final class Builder {
- private AnnotationElementSpec elementSpec;
+ private ElementCollector elementCollector;
private ProcessorOptionRegistry optionRegistry = new ProcessorOptionRegistry(List.of());
private String requiredSourceVersion;
@@ -64,11 +64,11 @@ private Builder() {}
/**
* Sets which elements the domain generates code for (required).
*
- * @param elementSpec the element specification
+ * @param elementCollector the collector declaring which elements to process
* @return this builder
*/
- public Builder elementSpec(AnnotationElementSpec elementSpec) {
- this.elementSpec = elementSpec;
+ public Builder elementCollector(ElementCollector elementCollector) {
+ this.elementCollector = elementCollector;
return this;
}
@@ -101,13 +101,14 @@ public Builder requiredSourceVersion(String requiredSourceVersion) {
* Builds the configuration.
*
* @return the configuration
- * @throws IllegalStateException when no {@link #elementSpec} was set
+ * @throws IllegalStateException when no {@link #elementCollector} was set
*/
public CodegenProcessorConfiguration build() {
- if (elementSpec == null) {
- throw new IllegalStateException("elementSpec is required");
+ if (elementCollector == null) {
+ throw new IllegalStateException("elementCollector is required");
}
- return new CodegenProcessorConfiguration(elementSpec, optionRegistry, requiredSourceVersion);
+ return new CodegenProcessorConfiguration(
+ elementCollector, optionRegistry, requiredSourceVersion);
}
}
}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollection.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollection.java
index 04382ccc..71ef1b3b 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollection.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollection.java
@@ -29,7 +29,7 @@
import javax.lang.model.element.Element;
/**
- * The result of {@link AnnotationElementSpec#collect}: per-selector-name, deterministically ordered
+ * The result of {@link ElementCollector#collect}: per-selector-name, deterministically ordered
* element lists. Elements are sorted by simple name so generated output is reproducible.
*/
public final class ElementCollection {
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
similarity index 95%
rename from processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
rename to processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
index 7326b413..3dd2a6b5 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AnnotationElementSpec.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
@@ -47,7 +47,7 @@
* responsibility in the planning step — per-instance configuration, repeatable-annotation expansion
* and conflict detection are domain semantics the spec does not know.
*/
-public final class AnnotationElementSpec {
+public final class ElementCollector {
private final List selectors;
@@ -56,7 +56,7 @@ public final class AnnotationElementSpec {
*
* @param selectors the named selection groups, in declaration order
*/
- public AnnotationElementSpec(List selectors) {
+ public ElementCollector(List selectors) {
this.selectors = selectors;
}
@@ -69,7 +69,7 @@ public static Builder builder() {
return new Builder();
}
- /** Fluent builder for {@link AnnotationElementSpec}. */
+ /** Fluent builder for {@link ElementCollector}. */
public static final class Builder {
private final List selectors = new ArrayList<>();
@@ -87,8 +87,8 @@ public Builder selector(Consumer selectorDeclaration) {
}
/** Builds the spec. */
- public AnnotationElementSpec build() {
- return new AnnotationElementSpec(selectors);
+ public ElementCollector build() {
+ return new ElementCollector(selectors);
}
}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
index ff439627..02b9c7da 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
@@ -28,7 +28,7 @@
import java.util.Set;
/**
- * One named element selection group inside {@link AnnotationElementSpec}.
+ * One named element selection group inside {@link ElementCollector}.
*
* An element is selected when it carries one of the {@link #annotationTypes}, or an annotation
* type that is itself meta-annotated with {@link #metaMarker} (the only way to discover
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 5f99f8b9..06784cd9 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -48,10 +48,10 @@
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
import org.javahelpers.codegen.processing.AbstractCodegenProcessor;
-import org.javahelpers.codegen.processing.AnnotationElementSpec;
import org.javahelpers.codegen.processing.CodegenProcessorConfiguration;
import org.javahelpers.codegen.processing.CodegenTask;
import org.javahelpers.codegen.processing.ElementCollection;
+import org.javahelpers.codegen.processing.ElementCollector;
import org.javahelpers.codegen.processing.ProcessorOptionRegistry;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
@@ -102,8 +102,8 @@ public class BuilderProcessor
* {@code @Ignore4BuilderGeneration}), plus classes declaring external types with
* {@code @SimpleBuilderFor}.
*/
- private static final AnnotationElementSpec ELEMENT_SPEC =
- AnnotationElementSpec.builder()
+ private static final ElementCollector ELEMENT_SPEC =
+ ElementCollector.builder()
.selector(
selector ->
selector
@@ -120,7 +120,7 @@ public class BuilderProcessor
/** The processor's static declarations handed to the framework driver. */
private static final CodegenProcessorConfiguration PROCESSOR_CONFIGURATION =
CodegenProcessorConfiguration.builder()
- .elementSpec(ELEMENT_SPEC)
+ .elementCollector(ELEMENT_SPEC)
.optionRegistry(OPTION_REGISTRY)
.requiredSourceVersion("RELEASE_17")
.build();
From 7d68150a61bb099433ef5c2838e2eccc2f42c04d Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 20:00:56 +0000
Subject: [PATCH 058/170] =?UTF-8?q?processing:=20backend=20wiring=20is=20t?=
=?UTF-8?q?he=20domain's=20business=20=E2=80=94=20back=20to=20direct=20usa?=
=?UTF-8?q?ge,=20no=20ServiceLoader=20registry?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 37 ++-------
.../ClassCodeGeneratorProvider.java | 44 -----------
.../processing/CodegenBackendRegistry.java | 75 -------------------
.../builders/processor/BuilderProcessor.java | 23 ++++++
.../roaster/RoasterCodeGeneratorProvider.java | 38 ----------
.../roaster/RoasterFormatterFactory.java | 39 ----------
...egen.processing.ClassCodeGeneratorProvider | 1 -
...g.SourceFormatterResolver$FormatterFactory | 1 -
8 files changed, 31 insertions(+), 227 deletions(-)
delete mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/ClassCodeGeneratorProvider.java
delete mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java
delete mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGeneratorProvider.java
delete mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterFormatterFactory.java
delete mode 100644 processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.ClassCodeGeneratorProvider
delete mode 100644 processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.SourceFormatterResolver$FormatterFactory
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index b35f663b..439daa81 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -38,7 +38,6 @@
import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper;
import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException;
-import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext;
@@ -107,7 +106,6 @@ public abstract class AbstractCodegenProcessor<
protected ProcessingLogger logger;
private final CodegenProcessorConfiguration processorConfiguration;
- private final CodegenBackendRegistry backendRegistry = new CodegenBackendRegistry();
private ClassCodeGenerator codeGenerator;
private SourceFormatterResolver formatterResolver;
private boolean supportedJdk = true;
@@ -363,11 +361,6 @@ private ProcessingLogger createLogger(
return logger;
}
- /** The backend registry resolving the code-generation engine and formatter factory. */
- protected CodegenBackendRegistry backendRegistry() {
- return backendRegistry;
- }
-
/**
* Creates the domain's processing context.
*
@@ -380,32 +373,18 @@ protected abstract C createContext(
ProcessingEnvironment processingEnv, CompilerArgumentsReader reader, ProcessingLogger logger);
/**
- * Creates the code generation engine — the {@link ClassCodeGenerator} backend port. Default: the
- * engine discovered by {@link CodegenBackendRegistry} over a {@link GenerationEnvironment} wired
- * from the processing environment and context.
+ * Creates the code generation engine — the {@link ClassCodeGenerator} backend port the domain
+ * wires over the processing environment, logger and context.
*/
- protected ClassCodeGenerator createGenerator(
- ProcessingEnvironment processingEnv, ProcessingLogger logger, C context) {
- return backendRegistry.resolveCodeGenerator(
- new GenerationEnvironment(
- processingEnv,
- logger,
- context.getPerformanceTracker(),
- context::reportBasedOnStrictMode));
- }
+ protected abstract ClassCodeGenerator createGenerator(
+ ProcessingEnvironment processingEnv, ProcessingLogger logger, C context);
/**
- * Creates the source formatter resolver for the configured profile. Default: the profile from
- * {@link ProcessorOptionRegistry#formatterProfile} and the factory discovered by {@link
- * CodegenBackendRegistry}.
+ * Creates the source formatter resolver for the configured profile — the domain supplies the
+ * profile name and the formatter factory matching its backend.
*/
- protected SourceFormatterResolver createFormatterResolver(
- CompilerArgumentsReader reader, ProcessingLogger logger) {
- return new SourceFormatterResolver(
- logger,
- optionRegistry().formatterProfile(reader),
- backendRegistry.resolveFormatterFactory());
- }
+ protected abstract SourceFormatterResolver createFormatterResolver(
+ CompilerArgumentsReader reader, ProcessingLogger logger);
private ElementCollector elementCollector() {
return processorConfiguration.elementCollector();
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ClassCodeGeneratorProvider.java b/processor/src/main/java/org/javahelpers/codegen/processing/ClassCodeGeneratorProvider.java
deleted file mode 100644
index 2095ecea..00000000
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ClassCodeGeneratorProvider.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025-2026 Andreas Igel
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package org.javahelpers.codegen.processing;
-
-import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
-import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
-
-/**
- * Service-provider interface for a code-generation backend: providers are discovered via {@link
- * java.util.ServiceLoader} and create the {@link ClassCodeGenerator} engine the driver generates
- * through. Register providers in {@code META-INF/services}.
- */
-public interface ClassCodeGeneratorProvider {
-
- /**
- * Creates the code-generation engine for the given generation environment.
- *
- * @param environment the generation environment (filer, logger, metrics, issue reporting)
- * @return the engine instance
- */
- ClassCodeGenerator create(GenerationEnvironment environment);
-}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java
deleted file mode 100644
index 67c5eb74..00000000
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenBackendRegistry.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025-2026 Andreas Igel
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package org.javahelpers.codegen.processing;
-
-import java.util.ServiceLoader;
-import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
-import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
-import org.javahelpers.simple.builders.processor.classgen.SourceFormatter;
-
-/**
- * Resolution point for pluggable code-generation backends: discovers the {@link ClassCodeGenerator}
- * engine and the {@link SourceFormatter} factory via {@link ServiceLoader}, so a backend library
- * only has to ship provider entries in {@code META-INF/services}.
- *
- *
The first provider on the classpath wins; explicit selection and priority ordering are a later
- * topic.
- */
-public final class CodegenBackendRegistry {
-
- /**
- * Resolves the code-generation engine from the discovered providers.
- *
- * @param environment the generation environment handed to the engine
- * @return the engine instance
- * @throws IllegalStateException when no provider is on the annotation processor path
- */
- public ClassCodeGenerator resolveCodeGenerator(GenerationEnvironment environment) {
- for (ClassCodeGeneratorProvider provider :
- ServiceLoader.load(
- ClassCodeGeneratorProvider.class, ClassCodeGeneratorProvider.class.getClassLoader())) {
- return provider.create(environment);
- }
- throw new IllegalStateException(
- "No ClassCodeGeneratorProvider found on the annotation processor path");
- }
-
- /**
- * Resolves the source-formatter factory from the discovered providers.
- *
- * @return the formatter factory
- * @throws IllegalStateException when no provider is on the annotation processor path
- */
- public SourceFormatterResolver.FormatterFactory resolveFormatterFactory() {
- for (SourceFormatterResolver.FormatterFactory factory :
- ServiceLoader.load(
- SourceFormatterResolver.FormatterFactory.class,
- SourceFormatterResolver.FormatterFactory.class.getClassLoader())) {
- return factory;
- }
- throw new IllegalStateException(
- "No SourceFormatterResolver.FormatterFactory found on the annotation processor path");
- }
-}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 06784cd9..3eacea9f 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -47,18 +47,24 @@
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
+import org.apache.commons.lang3.StringUtils;
import org.javahelpers.codegen.processing.AbstractCodegenProcessor;
import org.javahelpers.codegen.processing.CodegenProcessorConfiguration;
import org.javahelpers.codegen.processing.CodegenTask;
import org.javahelpers.codegen.processing.ElementCollection;
import org.javahelpers.codegen.processing.ElementCollector;
import org.javahelpers.codegen.processing.ProcessorOptionRegistry;
+import org.javahelpers.codegen.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFors;
import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser;
import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper;
+import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
+import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
+import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator;
+import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterSourceFormatter;
import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
@@ -141,6 +147,23 @@ protected String provideLoggerPrefix() {
return "simple-builders";
}
+ @Override
+ protected ClassCodeGenerator createGenerator(
+ ProcessingEnvironment processingEnv, ProcessingLogger logger, BuilderProcessingContext ctx) {
+ return new RoasterCodeGenerator(
+ new GenerationEnvironment(
+ processingEnv, logger, ctx.getPerformanceTracker(), ctx::reportBasedOnStrictMode));
+ }
+
+ @Override
+ protected SourceFormatterResolver createFormatterResolver(
+ CompilerArgumentsReader reader, ProcessingLogger logger) {
+ return new SourceFormatterResolver(
+ logger,
+ StringUtils.trimToNull(reader.readValue(CompilerArgumentsEnum.FORMATTER_PROFILE)),
+ RoasterSourceFormatter::new);
+ }
+
@Override
protected BuilderProcessingContext createContext(
ProcessingEnvironment processingEnv,
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGeneratorProvider.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGeneratorProvider.java
deleted file mode 100644
index 592a438f..00000000
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGeneratorProvider.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025-2026 Andreas Igel
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package org.javahelpers.simple.builders.processor.classgen.roaster;
-
-import org.javahelpers.codegen.processing.ClassCodeGeneratorProvider;
-import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
-import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
-
-/** {@link ClassCodeGeneratorProvider} for the Roaster backend, discovered via ServiceLoader. */
-public final class RoasterCodeGeneratorProvider implements ClassCodeGeneratorProvider {
-
- @Override
- public ClassCodeGenerator create(GenerationEnvironment environment) {
- return new RoasterCodeGenerator(environment);
- }
-}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterFormatterFactory.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterFormatterFactory.java
deleted file mode 100644
index 7f8aba91..00000000
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterFormatterFactory.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025-2026 Andreas Igel
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package org.javahelpers.simple.builders.processor.classgen.roaster;
-
-import org.javahelpers.codegen.processing.SourceFormatterResolver;
-import org.javahelpers.simple.builders.core.enums.FormattingMode;
-import org.javahelpers.simple.builders.processor.classgen.SourceFormatter;
-import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
-
-/** Roaster {@link SourceFormatter} factory, discovered via ServiceLoader. */
-public final class RoasterFormatterFactory implements SourceFormatterResolver.FormatterFactory {
-
- @Override
- public SourceFormatter create(ProcessingLogger logger, FormattingMode mode, String profile) {
- return new RoasterSourceFormatter(logger, mode, profile);
- }
-}
diff --git a/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.ClassCodeGeneratorProvider b/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.ClassCodeGeneratorProvider
deleted file mode 100644
index 982b7a50..00000000
--- a/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.ClassCodeGeneratorProvider
+++ /dev/null
@@ -1 +0,0 @@
-org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGeneratorProvider
diff --git a/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.SourceFormatterResolver$FormatterFactory b/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.SourceFormatterResolver$FormatterFactory
deleted file mode 100644
index 93bc555d..00000000
--- a/processor/src/main/resources/META-INF/services/org.javahelpers.codegen.processing.SourceFormatterResolver$FormatterFactory
+++ /dev/null
@@ -1 +0,0 @@
-org.javahelpers.simple.builders.processor.classgen.roaster.RoasterFormatterFactory
From a904a1b69120141bfa864af38df02b3d076915f0 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 20:02:56 +0000
Subject: [PATCH 059/170] tests: the drain error is generic 'target'; the
domain's plan-time noun stays 'builder'
---
.../builders/processor/BuilderConfigurationReaderTest.java | 4 ++--
.../simple/builders/processor/JacksonModuleWarningTest.java | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
index 6b844d98..81f28fa5 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
@@ -646,7 +646,7 @@ public class PersonDto {
// Then: Builder generation fails but compilation succeeds (no invalid Java code generated)
assertThat(compilation).succeeded();
- assertThat(compilation).hadWarningContaining("Failed to generate target");
+ assertThat(compilation).hadWarningContaining("Failed to generate builder");
assertThat(compilation).hadWarningContaining("builderAccess=PRIVATE");
assertThat(compilation).hadWarningContaining("Java does not allow private top-level classes");
}
@@ -682,7 +682,7 @@ public class PersonDto {
// Then: Builder generation fails but compilation succeeds (no invalid Java code generated)
assertThat(compilation).succeeded();
- assertThat(compilation).hadWarningContaining("Failed to generate target");
+ assertThat(compilation).hadWarningContaining("Failed to generate builder");
assertThat(compilation).hadWarningContaining("methodAccess=PRIVATE");
assertThat(compilation).hadWarningContaining("makes all setter methods inaccessible");
}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java
index 78316643..a2e56f52 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java
@@ -129,7 +129,7 @@ void defaultMode_jacksonModuleFailure_isWarning_andCompilationSucceeds() {
.compile(dto(), collidingModule());
assertThat(compilation).succeeded();
- assertThat(compilation).hadWarningContaining("Error generating Jackson module");
+ assertThat(compilation).hadWarningContaining("Error generating target");
}
@Test
@@ -143,6 +143,6 @@ void strictMode_jacksonModuleFailure_isError_andCompilationFails() {
.compile(dto(), collidingModule());
assertThat(compilation).failed();
- assertThat(compilation).hadErrorContaining("Error generating Jackson module");
+ assertThat(compilation).hadErrorContaining("Error generating target");
}
}
From 13adec5283bc7bed07d82d811ae9d0317a85518b Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 25 Oct 2026 20:05:23 +0000
Subject: [PATCH 060/170] =?UTF-8?q?processing:=20the=20per-target=20carrie?=
=?UTF-8?q?r=20resolves=20the=20output=20package=20=E2=80=94=20ProcessingT?=
=?UTF-8?q?argetPackage?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 9 +++--
.../codegen/processing/CodegenTask.java | 2 +-
...rget.java => ProcessingTargetPackage.java} | 2 +-
.../analysis/BuilderScopeResolver.java | 4 +-
.../AnnotationProcessingContext.java | 19 +++++-----
.../processing/BuilderProcessingContext.java | 4 +-
.../processor/BuilderScopeResolverTest.java | 38 +++++++++++--------
7 files changed, 44 insertions(+), 34 deletions(-)
rename processor/src/main/java/org/javahelpers/codegen/processing/{ProcessingTarget.java => ProcessingTargetPackage.java} (95%)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 439daa81..95a516f8 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -182,8 +182,8 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
context.debugStartOperation("Processing element: " + task.element().getSimpleName());
tracker.startClass(task.element().getSimpleName().toString());
try {
- context.initProcessingTarget(
- new ProcessingTarget<>(task.configuration(), task.outputPackage()));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage<>(task.configuration(), task.outputPackage()));
tracker.startPhase();
List definitions;
try {
@@ -219,8 +219,9 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
context.debugStartOperation(
"Generating targets for: " + work.task().element().getSimpleName());
try {
- context.initProcessingTarget(
- new ProcessingTarget<>(work.task().configuration(), work.task().outputPackage()));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage<>(
+ work.task().configuration(), work.task().outputPackage()));
tracker.startPhase();
generateTarget(work.target());
tracker.endPhase("Code Generation");
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
index a1f70cf1..a4524082 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java
@@ -40,7 +40,7 @@
* externally declared types, otherwise the element itself
* @param outputPackage the package generated code is written to, or {@code null} when the domain
* does not carry one — planning computes it so the driver can initialize {@link
- * ProcessingTarget} before analysis
+ * ProcessingTargetPackage} before analysis
*/
public record CodegenTask(
Element element, K configuration, Element reportingElement, String outputPackage) {}
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTarget.java b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTargetPackage.java
similarity index 95%
rename from processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTarget.java
rename to processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTargetPackage.java
index 4bd11b5e..318e9356 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTarget.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTargetPackage.java
@@ -38,4 +38,4 @@
* here
* @param the domain configuration type
*/
-public record ProcessingTarget(T configuration, String targetPackage) {}
+public record ProcessingTargetPackage(T configuration, String targetPackage) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
index 42057c83..bae7f91d 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
@@ -98,7 +98,7 @@ public BuilderScopeResolver(BuilderProcessingContext context) {
* This method reads the configuration from the processing context via {@link
* org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext#getConfiguration()}.
* The caller must ensure that {@link
- * org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext#initProcessingTarget}
+ * org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext#initProcessingTargetPackage}
* has been invoked with the owner element's resolved configuration beforehand, so that
* per-element {@code builderUsagePackages} overrides are respected.
*
@@ -163,7 +163,7 @@ public ResolvedBuilder generatedBuilderContract(TypeName builder) {
*
Unlike {@link #resolveUsableBuilderType(TypeElement)}, this method takes the configuration
* as an explicit parameter rather than reading it from the processing context. This is because it
* is called during generation-plan resolution, before {@link
- * org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext#initProcessingTarget}
+ * org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext#initProcessingTargetPackage}
* has been invoked for the element, so the context does not yet hold the per-element
* configuration.
*
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
index fc1fa35a..f683df4d 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
@@ -40,7 +40,7 @@
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
-import org.javahelpers.codegen.processing.ProcessingTarget;
+import org.javahelpers.codegen.processing.ProcessingTargetPackage;
import org.javahelpers.simple.builders.processor.analysis.GeneratedTypes;
import org.javahelpers.simple.builders.processor.analysis.TypeMappingContext;
import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
@@ -87,7 +87,7 @@ public class AnnotationProcessingContext implements TypeMappingContext {
private String generatedPackageName;
/** Per-target processing state for the type currently being generated. */
- private ProcessingTarget> processingTarget;
+ private ProcessingTargetPackage> processingTargetPackage;
/**
* Class definitions the domain pushed for deferred last-round generation — drained by the
@@ -139,16 +139,17 @@ protected void setGeneratedPackageName(String generatedPackageName) {
}
/**
- * Initializes the per-target state ({@link ProcessingTarget}) for the type currently being
+ * Initializes the per-target state ({@link ProcessingTargetPackage}) for the type currently being
* generated. The processing driver calls this once per planned task before analysis starts, so
* downstream code can reach the current configuration and output package without threading both
* through every call.
*
- * @param processingTarget the resolved configuration and output package of the current target
+ * @param processingTargetPackage the resolved configuration and output package of the current
+ * target
*/
- public void initProcessingTarget(ProcessingTarget> processingTarget) {
- this.processingTarget = processingTarget;
- setGeneratedPackageName(processingTarget.targetPackage());
+ public void initProcessingTargetPackage(ProcessingTargetPackage> processingTargetPackage) {
+ this.processingTargetPackage = processingTargetPackage;
+ setGeneratedPackageName(processingTargetPackage.targetPackage());
}
/**
@@ -156,8 +157,8 @@ public void initProcessingTarget(ProcessingTarget> processingTarget) {
*
* @return the current target's configuration and output package
*/
- public ProcessingTarget> getProcessingTarget() {
- return processingTarget;
+ public ProcessingTargetPackage> getProcessingTargetPackage() {
+ return processingTargetPackage;
}
/**
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java
index fbb41550..b1a36463 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderProcessingContext.java
@@ -84,7 +84,7 @@ private static AnnotationProcessingOptions createOptions(
* @return the builder configuration for the target being processed
*/
public BuilderConfiguration getConfiguration() {
- return (BuilderConfiguration) getProcessingTarget().configuration();
+ return (BuilderConfiguration) getProcessingTargetPackage().configuration();
}
/**
@@ -93,7 +93,7 @@ public BuilderConfiguration getConfiguration() {
* @return the qualified package name of the generated builder
*/
public String getBuilderPackageName() {
- return getProcessingTarget().targetPackage();
+ return getProcessingTargetPackage().targetPackage();
}
/**
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java
index 383df899..489f556d 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java
@@ -41,7 +41,7 @@
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
-import org.javahelpers.codegen.processing.ProcessingTarget;
+import org.javahelpers.codegen.processing.ProcessingTargetPackage;
import org.javahelpers.simple.builders.core.enums.OptionState;
import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
@@ -1314,7 +1314,8 @@ public boolean process(Set extends TypeElement> annotations, RoundEnvironment
BuilderProcessingContext context =
new BuilderProcessingContext(
ProcessingLogger.create(processingEnv), BuilderConfiguration.DEFAULT, processingEnv);
- context.initProcessingTarget(new ProcessingTarget(configuration("lib", "Builder"), ""));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage(configuration("lib", "Builder"), ""));
BuilderScopeResolver resolver = context.getBuilderScopeResolver();
// Register the type as generated, mirroring the real processor which calls
// registerGeneratedBuilder before any resolution happens.
@@ -1324,52 +1325,59 @@ public boolean process(Set extends TypeElement> annotations, RoundEnvironment
second = resolver.resolveUsableBuilderType(helper);
// Clear registration before testing scope-only behavior
resolver.resetGeneratedBuilders();
- context.initProcessingTarget(
- new ProcessingTarget(configuration("other", "OtherBuilder"), ""));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage(configuration("other", "OtherBuilder"), ""));
afterConfigurationChange = resolver.resolveUsableBuilderType(helper);
- context.initProcessingTarget(new ProcessingTarget(usageOnlyConfiguration("other"), ""));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage(usageOnlyConfiguration("other"), ""));
beforeRegistration = resolver.resolveUsableBuilderType(helper);
// Registration alone is not enough — the type must be in scope
resolver.registerGeneratedBuilder(
new TypeName("lib", "LibHelper"), new TypeName("lib", "LibHelperBuilder"));
afterRegistration = resolver.resolveUsableBuilderType(helper);
// Clear registration for usage-scope classpath lookup tests
- context.initProcessingTarget(new ProcessingTarget(usageOnlyConfiguration("lib"), ""));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage(usageOnlyConfiguration("lib"), ""));
resolver.resetGeneratedBuilders();
usageBeforeRegistration = resolver.resolveUsableBuilderType(helper);
resolver.registerGeneratedBuilder(
new TypeName("lib", "LibHelper"), new TypeName("lib", "LibHelperBuilder"));
usageAfterRegistration = resolver.resolveUsableBuilderType(helper);
// Usage scope without @SimpleBuilder annotation — type existence check only
- context.initProcessingTarget(new ProcessingTarget(usageOnlyConfiguration("lib"), ""));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage(usageOnlyConfiguration("lib"), ""));
resolver.resetGeneratedBuilders();
usageWithoutAnnotation = resolver.resolveUsableBuilderType(helper);
// Usage scope with builderUsageSuffix="Factory"
- context.initProcessingTarget(
- new ProcessingTarget(usageWithSuffixConfiguration("lib", "Factory"), ""));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage(usageWithSuffixConfiguration("lib", "Factory"), ""));
usageWithSuffix = resolver.resolveUsableBuilderType(helper);
// Usage scope with default suffix (no builderUsageSuffix configured)
- context.initProcessingTarget(new ProcessingTarget(usageOnlyConfiguration("lib"), ""));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage(usageOnlyConfiguration("lib"), ""));
usageDefaultSuffix = resolver.resolveUsableBuilderType(helper);
// Generated builder in the same package as the referenced builder: package-private
// contract members are accessible
- context.initProcessingTarget(new ProcessingTarget(usageOnlyConfiguration("lib"), "lib"));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage(usageOnlyConfiguration("lib"), "lib"));
usagePackagePrivate = resolver.resolveUsableBuilderType(helper);
// usingExistingBuilders=DISABLED: existing builders are not reused, but a builder
// generated in the current round is own generation and still resolves
- context.initProcessingTarget(
- new ProcessingTarget(existingBuildersDisabledConfiguration("lib"), ""));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage(existingBuildersDisabledConfiguration("lib"), ""));
usageWithExistingDisabled = resolver.resolveUsableBuilderType(helper);
resolver.registerGeneratedBuilder(
new TypeName("lib", "LibHelper"), new TypeName("lib", "LibHelperBuilder"));
usageGeneratedWithExistingDisabled = resolver.resolveUsableBuilderType(helper);
// usingFieldFunctionPrefill=DISABLED: field functions are not used as prefilled path
- context.initProcessingTarget(new ProcessingTarget(prefillDisabledConfiguration("lib"), ""));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage(prefillDisabledConfiguration("lib"), ""));
resolver.resetGeneratedBuilders();
usagePrefillDisabled = resolver.resolveUsableBuilderType(helper);
// Builder-contract probe for java.lang.String: StringBuilder must not resolve because its
// only parameterless method returning String is toString(), an Object signature
- context.initProcessingTarget(new ProcessingTarget(usageOnlyConfiguration("java.lang"), ""));
+ context.initProcessingTargetPackage(
+ new ProcessingTargetPackage(usageOnlyConfiguration("java.lang"), ""));
resolver.resetGeneratedBuilders();
TypeElement stringElement =
processingEnv.getElementUtils().getTypeElement("java.lang.String");
From df6ba0236df623eba95e45a34221760655b2391d Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:51:54 +0000
Subject: [PATCH 061/170] processing: dead PHASE_* constants left over from the
literal pass
---
.../codegen/processing/AbstractCodegenProcessor.java | 8 --------
1 file changed, 8 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 95a516f8..daf8f983 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -91,14 +91,6 @@ public abstract class AbstractCodegenProcessor<
/** Performance phase target generation is reported under — framework-owned. */
- /** Default performance phase for {@link #analyse} — domains may name their own. */
- private static final String PHASE_ANALYSIS = "Analysis";
-
- /**
- * Default performance phase for {@link #mapToTargetSpecification} — domains may name their own.
- */
- private static final String PHASE_TARGET_MAPPING = "Target Mapping";
-
/** The domain's context instance. */
protected C context;
From 40c04796df1776784917737889be08c79cabfdce Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:52:02 +0000
Subject: [PATCH 062/170] processing: the whole JDK-gate block moves into
verifySupportedJdk
---
.../processing/AbstractCodegenProcessor.java | 28 +++++++++++--------
1 file changed, 17 insertions(+), 11 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index daf8f983..038fdc9d 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -122,19 +122,25 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
this.codeGenerator = createGenerator(processingEnv, logger, context);
this.formatterResolver = createFormatterResolver(reader, logger);
- SourceVersion current = processingEnv.getSourceVersion();
- this.supportedJdk = isAtLeast(current, requiredSourceVersion());
- if (!this.supportedJdk) {
- reportUnsupportedJdk(current);
- }
+ this.supportedJdk = verifySupportedJdk();
}
- /** Reports that the running JDK cannot meet the required source level. */
- private void reportUnsupportedJdk(SourceVersion current) {
- String releaseNumber = requiredSourceVersion().replace("RELEASE_", "");
- context.error(
- "%s requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor.",
- logger.getTag(), releaseNumber, current, releaseNumber);
+ /**
+ * Checks the running JDK against {@link #requiredSourceVersion} and reports a descriptive error
+ * when it cannot meet the requirement.
+ *
+ * @return {@code true} when the running JDK supports the required source level
+ */
+ private boolean verifySupportedJdk() {
+ SourceVersion current = processingEnv.getSourceVersion();
+ boolean supported = isAtLeast(current, requiredSourceVersion());
+ if (!supported) {
+ String releaseNumber = requiredSourceVersion().replace("RELEASE_", "");
+ context.error(
+ "%s requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor.",
+ logger.getTag(), releaseNumber, current, releaseNumber);
+ }
+ return supported;
}
/**
From d3ef8cb02652226d8613711e7e5837e4aad791a3 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:52:20 +0000
Subject: [PATCH 063/170] =?UTF-8?q?processing:=20findElements=20needs=20no?=
=?UTF-8?q?=20context=20=E2=80=94=20the=20collector=20signature=20carries?=
=?UTF-8?q?=20logger=20only?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/AbstractCodegenProcessor.java | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 038fdc9d..edd30ad1 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -59,7 +59,7 @@
*
*
* ctx = createContext(env, reader, logger) [implement — in init]
- * elements = findElements(annotations, roundEnv, ctx) [default: configuration's elementCollector]
+ * elements = findElements(annotations, roundEnv) [default: configuration's elementCollector]
* tasks = planTasks(elements, ctx) [implement]
* registerPlannedTaskResults(tasks, ctx) [framework; per-task names from plannedTaskResultTypeNames() — implement]
* loop 1 — specification (guarded, strict-mode reporting): [framework]
@@ -165,7 +165,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
logger.infoTagged("PROCESSING ROUND START");
tracker.startPhase();
- ElementCollection collected = findElements(annotations, roundEnv, context);
+ ElementCollection collected = findElements(annotations, roundEnv);
tracker.endPhase("Element Collection");
logger.debugTagged("Processing round started.");
@@ -432,11 +432,10 @@ private boolean isAtLeast(SourceVersion current, String requiredName) {
*
* @param annotations the annotation types requested this round
* @param roundEnv the round environment
- * @param context the processing context
* @return the collected elements
*/
protected ElementCollection findElements(
- Set extends TypeElement> annotations, RoundEnvironment roundEnv, C context) {
+ Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
return elementCollector().collect(annotations, roundEnv, logger);
}
From 3827a428e00344aeac5361c90209675cab3ea0aa Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:52:33 +0000
Subject: [PATCH 064/170] =?UTF-8?q?processing:=20options=20come=20from=20t?=
=?UTF-8?q?he=20declared=20registry=20=E2=80=94=20no=20second=20channel?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 16 +---------------
1 file changed, 1 insertion(+), 15 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index edd30ad1..6851197e 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -25,7 +25,6 @@
package org.javahelpers.codegen.processing;
import java.util.ArrayList;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@@ -248,9 +247,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
@Override
public final Set getSupportedOptions() {
- Set options = new LinkedHashSet<>(optionRegistry().supportedOptions());
- options.addAll(additionalSupportedOptions());
- return options;
+ return optionRegistry().supportedOptions();
}
@Override
@@ -272,17 +269,6 @@ protected Optional> supportedAnnotationTypesByAnnotation() {
: Optional.empty();
}
- /**
- * Additional supported option names beyond the declared registry — for wrapper or embedder
- * processors adding their own options without redeclaring the domain's set (the seam MapStruct
- * uses for its processor wrappers). Default: none.
- *
- * @return extra option names
- */
- protected Set additionalSupportedOptions() {
- return Set.of();
- }
-
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
From d658d0784fc6880bb0b853274c2e77a80d3621ef Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:52:43 +0000
Subject: [PATCH 065/170] =?UTF-8?q?processing:=20declared=20options=20are?=
=?UTF-8?q?=20a=20set=20=E2=80=94=20duplicates=20cannot=20be=20declared=20?=
=?UTF-8?q?twice?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/ProcessorOptionRegistry.java | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
index a1681470..129ae801 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
@@ -25,7 +25,9 @@
package org.javahelpers.codegen.processing;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
@@ -41,7 +43,7 @@
*/
public final class ProcessorOptionRegistry {
- private final List options;
+ private final Set options;
private final CompilerOption verboseOption;
private final CompilerOption formatterProfileOption;
@@ -49,7 +51,7 @@ private ProcessorOptionRegistry(
Collection extends CompilerOption> options,
CompilerOption verboseOption,
CompilerOption formatterProfileOption) {
- this.options = List.copyOf(options);
+ this.options = Collections.unmodifiableSet(new LinkedHashSet<>(options));
this.verboseOption = verboseOption;
this.formatterProfileOption = formatterProfileOption;
}
From ac76e2c08df4065beaf097842a906552aa2d52db Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:52:51 +0000
Subject: [PATCH 066/170] processing: the wired verbose/formatter-profile
options are supported options too
---
.../codegen/processing/ProcessorOptionRegistry.java | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
index 129ae801..34b90a57 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java
@@ -77,7 +77,8 @@ public static Builder builder() {
/**
* All supported option names: both the javac-style {@code -A} option name and the legacy
- * compiler-argument spelling of every declared option.
+ * compiler-argument spelling of every declared option, including the framework-wired verbose and
+ * formatter-profile options.
*
* @return the supported option names for {@code AbstractProcessor.getSupportedOptions()}
*/
@@ -87,6 +88,12 @@ public Set supportedOptions() {
supported.add(option.getOptionName());
supported.add(option.getCompilerArgument());
}
+ for (CompilerOption wired : List.of(verboseOption, formatterProfileOption)) {
+ if (wired != null) {
+ supported.add(wired.getOptionName());
+ supported.add(wired.getCompilerArgument());
+ }
+ }
return supported;
}
From aa79d605070ad9ce650b5cd2255442a11d427a54 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:53:13 +0000
Subject: [PATCH 067/170] =?UTF-8?q?processing:=20the=20selector's=20key=20?=
=?UTF-8?q?is=20a=20group=20name=20=E2=80=94=20name=20renamed=20to=20group?=
=?UTF-8?q?Name?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/ElementCollector.java | 2 +-
.../codegen/processing/ElementSelector.java | 12 ++++++------
.../simple/builders/processor/BuilderProcessor.java | 8 ++++----
3 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
index 3dd2a6b5..352a7e32 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
@@ -126,7 +126,7 @@ public ElementCollection collect(
});
}
groups.put(
- selector.name(),
+ selector.groupName(),
selected.stream()
.sorted(Comparator.comparing(element -> element.getSimpleName().toString()))
.toList());
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
index 02b9c7da..33633bf0 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
@@ -35,13 +35,13 @@
* user-defined annotation types, since the supported annotation types stay {@code "*"}). Selected
* elements carrying the {@link #optOutAnnotation} are dropped again.
*
- * @param name the group name results are keyed by in {@link ElementCollection}
+ * @param groupName the group name results are keyed by in {@link ElementCollection}
* @param annotationTypes annotation types that directly select an element
* @param metaMarker meta-annotation marking annotation types that also select, or {@code null}
* @param optOutAnnotation annotation that excludes a selected element again, or {@code null}
*/
public record ElementSelector(
- String name,
+ String groupName,
Set> annotationTypes,
Class extends Annotation> metaMarker,
Class extends Annotation> optOutAnnotation) {
@@ -57,14 +57,14 @@ public static Builder builder() {
/** Fluent builder for {@link ElementSelector}. */
public static final class Builder {
- private String name;
+ private String groupName;
private Set> annotationTypes = Set.of();
private Class extends Annotation> metaMarker;
private Class extends Annotation> optOutAnnotation;
/** The group name results are keyed by. */
- public Builder name(String name) {
- this.name = name;
+ public Builder groupName(String groupName) {
+ this.groupName = groupName;
return this;
}
@@ -89,7 +89,7 @@ public Builder optOutAnnotation(Class extends Annotation> optOutAnnotation) {
/** Builds the selector. */
public ElementSelector build() {
- return new ElementSelector(name, annotationTypes, metaMarker, optOutAnnotation);
+ return new ElementSelector(groupName, annotationTypes, metaMarker, optOutAnnotation);
}
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 3eacea9f..ee4b8516 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -113,13 +113,13 @@ public class BuilderProcessor
.selector(
selector ->
selector
- .name(GROUP_ANNOTATED_ELEMENTS)
+ .groupName(GROUP_ANNOTATED_ELEMENTS)
.metaMarker(Template.class)
.optOutAnnotation(Ignore4BuilderGeneration.class))
.selector(
selector ->
selector
- .name(GROUP_EXTERNAL_TYPE_HOLDERS)
+ .groupName(GROUP_EXTERNAL_TYPE_HOLDERS)
.annotationTypes(SimpleBuilderFor.class, SimpleBuilderFors.class))
.build();
@@ -189,8 +189,8 @@ protected BuilderProcessingContext createContext(
@Override
protected List> planTasks(
ElementCollection collected, BuilderProcessingContext ctx) {
- List sortedElements = collected.byName(GROUP_ANNOTATED_ELEMENTS);
- List sortedHolders = collected.byName(GROUP_EXTERNAL_TYPE_HOLDERS);
+ List sortedElements = collected.byGroup(GROUP_ANNOTATED_ELEMENTS);
+ List sortedHolders = collected.byGroup(GROUP_EXTERNAL_TYPE_HOLDERS);
ctx.debug("simple-builders: Found %d annotated elements.", sortedElements.size());
ctx.debug(
"simple-builders: Found %d type(s) for generation with @SimpleBuilderFor.",
From 48877eb5e24af1da5045bf962b6d5fc995d258bf Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:53:30 +0000
Subject: [PATCH 068/170] =?UTF-8?q?processing:=20the=20collection=20is=20g?=
=?UTF-8?q?roups=20=E2=80=94=20ElementGroups=20keyed=20by=20selector=20gro?=
=?UTF-8?q?up=20names?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/AbstractCodegenProcessor.java | 8 ++++----
.../javahelpers/codegen/processing/ElementCollector.java | 4 ++--
.../{ElementCollection.java => ElementGroups.java} | 8 ++++----
.../javahelpers/codegen/processing/ElementSelector.java | 2 +-
.../simple/builders/processor/BuilderProcessor.java | 4 ++--
5 files changed, 13 insertions(+), 13 deletions(-)
rename processor/src/main/java/org/javahelpers/codegen/processing/{ElementCollection.java => ElementGroups.java} (90%)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 6851197e..5e5a4ea7 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -164,7 +164,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
logger.infoTagged("PROCESSING ROUND START");
tracker.startPhase();
- ElementCollection collected = findElements(annotations, roundEnv);
+ ElementGroups collected = findElements(annotations, roundEnv);
tracker.endPhase("Element Collection");
logger.debugTagged("Processing round started.");
@@ -420,7 +420,7 @@ private boolean isAtLeast(SourceVersion current, String requiredName) {
* @param roundEnv the round environment
* @return the collected elements
*/
- protected ElementCollection findElements(
+ protected ElementGroups findElements(
Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
return elementCollector().collect(annotations, roundEnv, logger);
}
@@ -434,7 +434,7 @@ protected ElementCollection findElements(
* @param context the processing context
* @return the round's tasks, or empty when the round is gated out
*/
- private List> plan(ElementCollection collected, C context) {
+ private List> plan(ElementGroups collected, C context) {
List> tasks = planTasks(collected, context);
if (collected.isEmpty() || isFinished(context)) {
return List.of();
@@ -456,7 +456,7 @@ private List> plan(ElementCollection collected, C context) {
* @param context the processing context
* @return the generation plan, possibly empty
*/
- protected abstract List> planTasks(ElementCollection collected, C context);
+ protected abstract List> planTasks(ElementGroups collected, C context);
/**
* Registers the types this round will generate, up front, so references to them resolve during
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
index 352a7e32..09c480b8 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
@@ -100,7 +100,7 @@ public ElementCollector build() {
* @param logger logger for opt-out diagnostics
* @return the per-selector element groups, sorted by simple name
*/
- public ElementCollection collect(
+ public ElementGroups collect(
Set extends TypeElement> annotations, RoundEnvironment roundEnv, ProcessingLogger logger) {
Map> groups = new LinkedHashMap<>();
for (ElementSelector selector : selectors) {
@@ -131,7 +131,7 @@ public ElementCollection collect(
.sorted(Comparator.comparing(element -> element.getSimpleName().toString()))
.toList());
}
- return new ElementCollection(groups);
+ return new ElementGroups(groups);
}
private static List matchingAnnotations(
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollection.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementGroups.java
similarity index 90%
rename from processor/src/main/java/org/javahelpers/codegen/processing/ElementCollection.java
rename to processor/src/main/java/org/javahelpers/codegen/processing/ElementGroups.java
index 71ef1b3b..84d7d826 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollection.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementGroups.java
@@ -32,11 +32,11 @@
* The result of {@link ElementCollector#collect}: per-selector-name, deterministically ordered
* element lists. Elements are sorted by simple name so generated output is reproducible.
*/
-public final class ElementCollection {
+public final class ElementGroups {
private final Map> groups;
- ElementCollection(Map> groups) {
+ ElementGroups(Map> groups) {
this.groups = groups;
}
@@ -46,8 +46,8 @@ public final class ElementCollection {
* @param selectorName name of an {@link ElementSelector}
* @return the selected elements, or an empty list when the selector matched nothing
*/
- public List byName(String selectorName) {
- return groups.getOrDefault(selectorName, List.of());
+ public List byGroup(String groupName) {
+ return groups.getOrDefault(groupName, List.of());
}
/**
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
index 33633bf0..2b7f82dd 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java
@@ -35,7 +35,7 @@
* user-defined annotation types, since the supported annotation types stay {@code "*"}). Selected
* elements carrying the {@link #optOutAnnotation} are dropped again.
*
- * @param groupName the group name results are keyed by in {@link ElementCollection}
+ * @param groupName the group name results are keyed by in {@link ElementGroups}
* @param annotationTypes annotation types that directly select an element
* @param metaMarker meta-annotation marking annotation types that also select, or {@code null}
* @param optOutAnnotation annotation that excludes a selected element again, or {@code null}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index ee4b8516..7642b9d6 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -51,8 +51,8 @@
import org.javahelpers.codegen.processing.AbstractCodegenProcessor;
import org.javahelpers.codegen.processing.CodegenProcessorConfiguration;
import org.javahelpers.codegen.processing.CodegenTask;
-import org.javahelpers.codegen.processing.ElementCollection;
import org.javahelpers.codegen.processing.ElementCollector;
+import org.javahelpers.codegen.processing.ElementGroups;
import org.javahelpers.codegen.processing.ProcessorOptionRegistry;
import org.javahelpers.codegen.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
@@ -188,7 +188,7 @@ protected BuilderProcessingContext createContext(
@Override
protected List> planTasks(
- ElementCollection collected, BuilderProcessingContext ctx) {
+ ElementGroups collected, BuilderProcessingContext ctx) {
List sortedElements = collected.byGroup(GROUP_ANNOTATED_ELEMENTS);
List sortedHolders = collected.byGroup(GROUP_EXTERNAL_TYPE_HOLDERS);
ctx.debug("simple-builders: Found %d annotated elements.", sortedElements.size());
From 10e196dde723119231ca4c2ca27d9cbf1580f6aa Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:53:38 +0000
Subject: [PATCH 069/170] =?UTF-8?q?the=20constant=20holds=20an=20ElementCo?=
=?UTF-8?q?llector=20=E2=80=94=20ELEMENT=5FCOLLECTOR?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../simple/builders/processor/BuilderProcessor.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 7642b9d6..4ed75524 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -108,7 +108,7 @@ public class BuilderProcessor
* {@code @Ignore4BuilderGeneration}), plus classes declaring external types with
* {@code @SimpleBuilderFor}.
*/
- private static final ElementCollector ELEMENT_SPEC =
+ private static final ElementCollector ELEMENT_COLLECTOR =
ElementCollector.builder()
.selector(
selector ->
@@ -126,7 +126,7 @@ public class BuilderProcessor
/** The processor's static declarations handed to the framework driver. */
private static final CodegenProcessorConfiguration PROCESSOR_CONFIGURATION =
CodegenProcessorConfiguration.builder()
- .elementCollector(ELEMENT_SPEC)
+ .elementCollector(ELEMENT_COLLECTOR)
.optionRegistry(OPTION_REGISTRY)
.requiredSourceVersion("RELEASE_17")
.build();
From b7b9ab4e734a5090eef18bfa9439ed887dfb0b37 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:53:51 +0000
Subject: [PATCH 070/170] =?UTF-8?q?processing:=20PlannedWork=20=E2=86=92?=
=?UTF-8?q?=20PlannedGeneration=20=E2=80=94=20protected=20so=20domain=20ca?=
=?UTF-8?q?llbacks=20can=20consume=20it?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/AbstractCodegenProcessor.java | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 5e5a4ea7..6e019c4b 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -174,7 +174,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
}
// First loop: specification — analyse every task and map its render targets.
- List> planned = new ArrayList<>();
+ List> planned = new ArrayList<>();
for (CodegenTask task : tasks) {
context.debugStartOperation("Processing element: " + task.element().getSimpleName());
tracker.startClass(task.element().getSimpleName().toString());
@@ -196,7 +196,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
} finally {
tracker.endPhase("Target Mapping");
}
- planned.add(new PlannedWork<>(task, definition, target));
+ planned.add(new PlannedGeneration<>(task, definition, target));
}
} catch (Exception ex) {
// By default generation failures are warnings so other targets are still generated.
@@ -212,7 +212,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
// Second loop: generation — every specification exists before any file is written.
int successfulGenerations = 0;
- for (PlannedWork work : planned) {
+ for (PlannedGeneration work : planned) {
context.debugStartOperation(
"Generating targets for: " + work.task().element().getSimpleName());
try {
@@ -579,6 +579,6 @@ protected void onProcessingOver(C context) {
protected void endRound(C context) {}
/** One task's specification output, carried from the analysis loop into the generation loop. */
- private record PlannedWork(
+ private record PlannedGeneration(
CodegenTask task, D definition, GenerationTargetClassDto target) {}
}
From 55cae315b2b62847eaded04ea73d2587fd726d27 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:54:15 +0000
Subject: [PATCH 071/170] processing: the generated-task callback consumes the
whole planned generation
---
.../processing/AbstractCodegenProcessor.java | 19 ++++++++++---------
.../builders/processor/BuilderProcessor.java | 9 ++++-----
2 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 6e019c4b..b5e3a644 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -67,7 +67,7 @@
* loop 2 — generation: [framework]
* for each target:
* existsGuard → engine.generate(target, formatterResolver.resolve(target.mode)) [framework]
- * onTaskGenerated(def, target, task, ctx) [default: no-op]
+ * onTaskGenerated(generation, ctx) [default: no-op]
* endRound(ctx) [default: no-op]
* last round: onProcessingOver(ctx) [default: drains context.getDeferredTargets()]
*
@@ -222,7 +222,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
tracker.startPhase();
generateTarget(work.target());
tracker.endPhase("Code Generation");
- onTaskGenerated(work.definition(), work.target(), work.task(), context);
+ onTaskGenerated(work, context);
successfulGenerations++;
} catch (Exception ex) {
context.reportBasedOnStrictMode(
@@ -546,13 +546,10 @@ protected abstract GenerationTargetClassDto mapToTargetSpecification(
* Event after a task's target generated successfully — the insert point for deferred-output
* accumulation, per-class metrics, and domain bookkeeping. Default: nothing.
*
- * @param definition the analysed definition
- * @param target the generated class definition
- * @param task the planned task
+ * @param generation the planned generation — task, analysed definition and generated target
* @param context the processing context
*/
- protected void onTaskGenerated(
- D definition, GenerationTargetClassDto target, CodegenTask task, C context) {}
+ protected void onTaskGenerated(PlannedGeneration generation, C context) {}
/**
* Callback for the last processing round — generates every target the domain pushed onto the
@@ -578,7 +575,11 @@ protected void onProcessingOver(C context) {
/** Callback at the end of a generating round — domain finalization. */
protected void endRound(C context) {}
- /** One task's specification output, carried from the analysis loop into the generation loop. */
- private record PlannedGeneration(
+ /**
+ * One planned generation, carried from the analysis loop into the generation loop and handed to
+ * {@link #onTaskGenerated} as a whole — the record domains read for their generated-task
+ * callback.
+ */
+ protected record PlannedGeneration(
CodegenTask task, D definition, GenerationTargetClassDto target) {}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 4ed75524..92840a9f 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -501,11 +501,10 @@ protected GenerationTargetClassDto mapToTargetSpecification(
@Override
protected void onTaskGenerated(
- BuilderDefinitionDto definition,
- GenerationTargetClassDto target,
- CodegenTask task,
+ PlannedGeneration generation,
BuilderProcessingContext ctx) {
- GenerationTargetClassDto renderingDto = target;
+ BuilderDefinitionDto definition = generation.definition();
+ GenerationTargetClassDto renderingDto = generation.target();
int collectionCount =
(int)
definition.getAllFieldsForBuilder().stream()
@@ -517,7 +516,7 @@ protected void onTaskGenerated(
.count();
// Collect info for Jackson Module if enabled
- jacksonModuleGenerator.addEntry(definition, task.element());
+ jacksonModuleGenerator.addEntry(definition, generation.task().element());
ctx.debug("Jackson module entry added");
// End class-level timing (started by the driver before analysis)
From cffc6fe352ef9a72de9772d22bb2a9fdb32b75c7 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:55:59 +0000
Subject: [PATCH 072/170] =?UTF-8?q?no=20deferred-target=20mechanism=20?=
=?UTF-8?q?=E2=80=94=20the=20domain=20generates=20its=20last-round=20outpu?=
=?UTF-8?q?ts=20in=20onProcessingOver?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 27 +++++-------------
.../builders/processor/BuilderProcessor.java | 10 +++++--
.../AnnotationProcessingContext.java | 28 -------------------
.../processor/JacksonModuleWarningTest.java | 4 +--
4 files changed, 17 insertions(+), 52 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index b5e3a644..36ceed20 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -69,7 +69,7 @@
* existsGuard → engine.generate(target, formatterResolver.resolve(target.mode)) [framework]
* onTaskGenerated(generation, ctx) [default: no-op]
* endRound(ctx) [default: no-op]
- * last round: onProcessingOver(ctx) [default: drains context.getDeferredTargets()]
+ * last round: onProcessingOver(ctx) [default: no-op]
*
*
* Per-element failures are caught and reported through {@link
@@ -277,7 +277,7 @@ public SourceVersion getSupportedSourceVersion() {
/**
* Generates one target class through the configured engine after checking the target does not
* already exist. Also usable by the domain's {@link #onProcessingOver} implementation for
- * deferred outputs.
+ * last-round outputs.
*
* @param target the class definition to generate
* @throws CodeGenerationException if the target exists or generation fails
@@ -543,8 +543,8 @@ protected abstract GenerationTargetClassDto mapToTargetSpecification(
D definition, K configuration, C context) throws Exception;
/**
- * Event after a task's target generated successfully — the insert point for deferred-output
- * accumulation, per-class metrics, and domain bookkeeping. Default: nothing.
+ * Event after a task's target generated successfully — the insert point for per-class metrics and
+ * domain bookkeeping. Default: nothing.
*
* @param generation the planned generation — task, analysed definition and generated target
* @param context the processing context
@@ -552,25 +552,12 @@ protected abstract GenerationTargetClassDto mapToTargetSpecification(
protected void onTaskGenerated(PlannedGeneration generation, C context) {}
/**
- * Callback for the last processing round — generates every target the domain pushed onto the
- * context through {@link AnnotationProcessingContext#addDeferredTarget} via the same guarded path
- * as round targets. Domain extensions log and push their deferred outputs, then call {@code
- * super.onProcessingOver(context)} to drain, wrapped in their finalization (SPI flush, reports).
+ * Callback when processing is over — the domain's last-round finalization: SPI flush, reports,
+ * and last-round outputs generated directly through {@link #generateTarget}. Default: nothing.
*
* @param context the processing context
*/
- protected void onProcessingOver(C context) {
- for (GenerationTargetClassDto target : context.getDeferredTargets()) {
- try {
- generateTarget(target);
- } catch (Exception e) {
- context.reportBasedOnStrictMode(
- logger.prefixed("Error generating target for package %s: %s"),
- target.getTypeName().getPackageName(),
- e.getMessage());
- }
- }
- }
+ protected void onProcessingOver(C context) {}
/** Callback at the end of a generating round — domain finalization. */
protected void endRound(C context) {}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 92840a9f..8c8f9ffd 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -550,9 +550,15 @@ protected void onProcessingOver(BuilderProcessingContext ctx) {
ctx.info(
"simple-builders: Generating Jackson module in package '%s'",
moduleDefinition.getTypeName().getPackageName());
- ctx.addDeferredTarget(moduleDefinition);
+ try {
+ generateTarget(moduleDefinition);
+ } catch (Exception e) {
+ ctx.reportBasedOnStrictMode(
+ logger.prefixed("Error generating Jackson module for package %s: %s"),
+ moduleDefinition.getTypeName().getPackageName(),
+ e.getMessage());
+ }
}
- super.onProcessingOver(ctx);
// Reset indentation after Jackson module generation as well
ctx.resetIndentation();
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
index f683df4d..b4446013 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
@@ -24,7 +24,6 @@
package org.javahelpers.simple.builders.processor.processing;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -43,7 +42,6 @@
import org.javahelpers.codegen.processing.ProcessingTargetPackage;
import org.javahelpers.simple.builders.processor.analysis.GeneratedTypes;
import org.javahelpers.simple.builders.processor.analysis.TypeMappingContext;
-import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.logging.ActivePerformanceTracker;
import org.javahelpers.simple.builders.processor.processing.logging.NoOpPerformanceTracker;
@@ -89,12 +87,6 @@ public class AnnotationProcessingContext implements TypeMappingContext {
/** Per-target processing state for the type currently being generated. */
private ProcessingTargetPackage> processingTargetPackage;
- /**
- * Class definitions the domain pushed for deferred last-round generation — drained by the
- * processing driver in the last round.
- */
- private final List deferredTargets = new ArrayList<>();
-
/**
* Creates the context with default options (strict mode enabled, performance tracking disabled).
*
@@ -161,26 +153,6 @@ public ProcessingTargetPackage> getProcessingTargetPackage() {
return processingTargetPackage;
}
- /**
- * Queues a class definition for deferred generation in the last processing round. Domains push
- * during a round ({@code onTaskGenerated}, end-of-round or finalization events) and the driver
- * generates them once specification of every target has finished.
- *
- * @param target the deferred output and its diagnostic kind
- */
- public void addDeferredTarget(GenerationTargetClassDto target) {
- deferredTargets.add(target);
- }
-
- /**
- * The deferred outputs queued so far.
- *
- * @return the mutable deferred-target list
- */
- public List getDeferredTargets() {
- return deferredTargets;
- }
-
/**
* Checks whether a member (constructor, method) is accessible from the package generated code is
* written to.
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java
index a2e56f52..78316643 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java
@@ -129,7 +129,7 @@ void defaultMode_jacksonModuleFailure_isWarning_andCompilationSucceeds() {
.compile(dto(), collidingModule());
assertThat(compilation).succeeded();
- assertThat(compilation).hadWarningContaining("Error generating target");
+ assertThat(compilation).hadWarningContaining("Error generating Jackson module");
}
@Test
@@ -143,6 +143,6 @@ void strictMode_jacksonModuleFailure_isError_andCompilationFails() {
.compile(dto(), collidingModule());
assertThat(compilation).failed();
- assertThat(compilation).hadErrorContaining("Error generating target");
+ assertThat(compilation).hadErrorContaining("Error generating Jackson module");
}
}
From 1086235facbead7136e22c20ba9feb4d8efc2663 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:56:29 +0000
Subject: [PATCH 073/170] =?UTF-8?q?processing:=20a=20definition=20knows=20?=
=?UTF-8?q?its=20target=20type's=20name=20=E2=80=94=20the=20contract=20pla?=
=?UTF-8?q?nned=20names=20predict?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/CodegenDefinition.java | 12 ++++++++++--
.../processor/model/core/BuilderDefinitionDto.java | 5 +++++
2 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenDefinition.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenDefinition.java
index b7554992..20a2a83f 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenDefinition.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenDefinition.java
@@ -33,9 +33,17 @@
public interface CodegenDefinition {
/**
- * The type this definition was analysed from.
+ * The definition's primary type name — the analysed type's own name.
*
- * @return the analysed type's name
+ * @return the primary type name
*/
TypeName getTypeName();
+
+ /**
+ * The name of the type the definition's target will generate — the prediction a domain makes at
+ * task level, confirmed once the definition exists.
+ *
+ * @return the generated target type's name
+ */
+ TypeName targetTypeName();
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java
index e7158ad4..6876b2b8 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java
@@ -143,6 +143,11 @@ public TypeName getTypeName() {
return getBuilderTypeName();
}
+ @Override
+ public TypeName targetTypeName() {
+ return getBuilderTypeName();
+ }
+
public void setTypeName(TypeName typeName) {
setBuilderTypeName(typeName);
}
From d61326fb0174ef25a7f384c18c95cfd05ff4a8cd Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:57:15 +0000
Subject: [PATCH 074/170] processing: generationEnvironment wires
env/logger/tracker/reporter once in the base
---
.../processing/AbstractCodegenProcessor.java | 18 +++++++++++++++++-
.../builders/processor/BuilderProcessor.java | 5 +----
2 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 36ceed20..afb7443a 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -37,6 +37,7 @@
import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper;
import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException;
+import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext;
@@ -359,11 +360,26 @@ protected abstract C createContext(
/**
* Creates the code generation engine — the {@link ClassCodeGenerator} backend port the domain
- * wires over the processing environment, logger and context.
+ * wires over {@link #generationEnvironment}.
*/
protected abstract ClassCodeGenerator createGenerator(
ProcessingEnvironment processingEnv, ProcessingLogger logger, C context);
+ /**
+ * The {@link GenerationEnvironment} the engine runs against — processing environment, logger,
+ * performance tracker and the strict-mode reporter, wired from the context the driver created.
+ *
+ * @param processingEnv the processing environment
+ * @param logger the processing logger
+ * @param context the processing context
+ * @return the generation environment for the engine's constructor
+ */
+ protected GenerationEnvironment generationEnvironment(
+ ProcessingEnvironment processingEnv, ProcessingLogger logger, C context) {
+ return new GenerationEnvironment(
+ processingEnv, logger, context.getPerformanceTracker(), context::reportBasedOnStrictMode);
+ }
+
/**
* Creates the source formatter resolver for the configured profile — the domain supplies the
* profile name and the formatter factory matching its backend.
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 8c8f9ffd..424c4b0a 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -62,7 +62,6 @@
import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser;
import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper;
import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator;
-import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment;
import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator;
import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterSourceFormatter;
import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
@@ -150,9 +149,7 @@ protected String provideLoggerPrefix() {
@Override
protected ClassCodeGenerator createGenerator(
ProcessingEnvironment processingEnv, ProcessingLogger logger, BuilderProcessingContext ctx) {
- return new RoasterCodeGenerator(
- new GenerationEnvironment(
- processingEnv, logger, ctx.getPerformanceTracker(), ctx::reportBasedOnStrictMode));
+ return new RoasterCodeGenerator(generationEnvironment(processingEnv, logger, ctx));
}
@Override
From da6984819d102f1b675212c80dbabe9555ced4f6 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:57:52 +0000
Subject: [PATCH 075/170] =?UTF-8?q?processing:=20formatter=20factory=20is?=
=?UTF-8?q?=20a=20static=20declaration=20=E2=80=94=20base=20resolves=20pro?=
=?UTF-8?q?file=20and=20formatter?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 13 +++++++----
.../CodegenProcessorConfiguration.java | 22 +++++++++++++++++--
.../builders/processor/BuilderProcessor.java | 12 +---------
3 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index afb7443a..f691e32e 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -381,11 +381,16 @@ protected GenerationEnvironment generationEnvironment(
}
/**
- * Creates the source formatter resolver for the configured profile — the domain supplies the
- * profile name and the formatter factory matching its backend.
+ * Creates the source formatter resolver for the configured profile — profile name and formatter
+ * factory come from the processor's declared options and configuration.
*/
- protected abstract SourceFormatterResolver createFormatterResolver(
- CompilerArgumentsReader reader, ProcessingLogger logger);
+ protected SourceFormatterResolver createFormatterResolver(
+ CompilerArgumentsReader reader, ProcessingLogger logger) {
+ return new SourceFormatterResolver(
+ logger,
+ optionRegistry().formatterProfile(reader),
+ processorConfiguration.formatterFactory());
+ }
private ElementCollector elementCollector() {
return processorConfiguration.elementCollector();
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
index e75ad5cf..a893bdd3 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
@@ -25,6 +25,7 @@
package org.javahelpers.codegen.processing;
import java.util.List;
+import org.javahelpers.simple.builders.processor.classgen.SourceFormatter;
/**
* Static declarations of an {@link AbstractCodegenProcessor}: everything the framework needs to
@@ -37,11 +38,14 @@
* @param optionRegistry the processor's declared compiler options; default: an empty registry
* @param requiredSourceVersion the minimum source level by name (e.g. {@code "RELEASE_17"});
* default: the running JDK's latest supported release
+ * @param formatterFactory creates the {@link SourceFormatter} for a formatting mode and profile;
+ * default: a pass-through formatter leaving sources unchanged
*/
public record CodegenProcessorConfiguration(
ElementCollector elementCollector,
ProcessorOptionRegistry optionRegistry,
- String requiredSourceVersion) {
+ String requiredSourceVersion,
+ SourceFormatterResolver.FormatterFactory formatterFactory) {
/**
* Creates a builder with all defaults applied — only {@link #elementCollector(ElementCollector)}
@@ -58,6 +62,8 @@ public static final class Builder {
private ElementCollector elementCollector;
private ProcessorOptionRegistry optionRegistry = new ProcessorOptionRegistry(List.of());
private String requiredSourceVersion;
+ private SourceFormatterResolver.FormatterFactory formatterFactory =
+ (logger, mode, profile) -> raw -> raw;
private Builder() {}
@@ -97,6 +103,18 @@ public Builder requiredSourceVersion(String requiredSourceVersion) {
return this;
}
+ /**
+ * Sets the factory creating the {@link SourceFormatter} for a formatting mode and profile — the
+ * domain's formatter backend. Default: pass-through.
+ *
+ * @param formatterFactory the formatter factory
+ * @return this builder
+ */
+ public Builder formatterFactory(SourceFormatterResolver.FormatterFactory formatterFactory) {
+ this.formatterFactory = formatterFactory;
+ return this;
+ }
+
/**
* Builds the configuration.
*
@@ -108,7 +126,7 @@ public CodegenProcessorConfiguration build() {
throw new IllegalStateException("elementCollector is required");
}
return new CodegenProcessorConfiguration(
- elementCollector, optionRegistry, requiredSourceVersion);
+ elementCollector, optionRegistry, requiredSourceVersion, formatterFactory);
}
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 424c4b0a..21cce7ef 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -47,14 +47,12 @@
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
-import org.apache.commons.lang3.StringUtils;
import org.javahelpers.codegen.processing.AbstractCodegenProcessor;
import org.javahelpers.codegen.processing.CodegenProcessorConfiguration;
import org.javahelpers.codegen.processing.CodegenTask;
import org.javahelpers.codegen.processing.ElementCollector;
import org.javahelpers.codegen.processing.ElementGroups;
import org.javahelpers.codegen.processing.ProcessorOptionRegistry;
-import org.javahelpers.codegen.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;
@@ -128,6 +126,7 @@ public class BuilderProcessor
.elementCollector(ELEMENT_COLLECTOR)
.optionRegistry(OPTION_REGISTRY)
.requiredSourceVersion("RELEASE_17")
+ .formatterFactory(RoasterSourceFormatter::new)
.build();
/** Creates the processor — javac instantiates it reflectively. */
@@ -152,15 +151,6 @@ protected ClassCodeGenerator createGenerator(
return new RoasterCodeGenerator(generationEnvironment(processingEnv, logger, ctx));
}
- @Override
- protected SourceFormatterResolver createFormatterResolver(
- CompilerArgumentsReader reader, ProcessingLogger logger) {
- return new SourceFormatterResolver(
- logger,
- StringUtils.trimToNull(reader.readValue(CompilerArgumentsEnum.FORMATTER_PROFILE)),
- RoasterSourceFormatter::new);
- }
-
@Override
protected BuilderProcessingContext createContext(
ProcessingEnvironment processingEnv,
From fcf2ab1e7b865b433af0e16dc8a264f1531b9cd7 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 16:58:08 +0000
Subject: [PATCH 076/170] =?UTF-8?q?processing:=20metaMarked=20names=20what?=
=?UTF-8?q?=20the=20check=20is=20=E2=80=94=20meta-annotation=20on=20the=20?=
=?UTF-8?q?annotation=20type?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../org/javahelpers/codegen/processing/ElementCollector.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
index 09c480b8..ad858c3e 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
@@ -145,9 +145,9 @@ private static List matchingAnnotations(
selector.annotationTypes().stream()
.anyMatch(
type -> type.getCanonicalName().equals(annotation.getQualifiedName().toString()));
- boolean meta =
+ boolean metaMarked =
selector.metaMarker() != null && annotation.getAnnotation(selector.metaMarker()) != null;
- if (direct || meta) {
+ if (direct || metaMarked) {
result.add(annotation);
}
}
From 7d23ac998182ce31d07deaf03b6942ed24f343b7 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 17:16:13 +0000
Subject: [PATCH 077/170] processing: onRoundStart/onTasksPlanned hooks;
in-body extension-point comments
---
.../processing/AbstractCodegenProcessor.java | 43 +++++++++++++++++--
1 file changed, 39 insertions(+), 4 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index f691e32e..c2f2a842 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -59,8 +59,10 @@
*
*
* ctx = createContext(env, reader, logger) [implement — in init]
+ * onRoundStart(ctx) [default: no-op]
* elements = findElements(annotations, roundEnv) [default: configuration's elementCollector]
* tasks = planTasks(elements, ctx) [implement]
+ * onTasksPlanned(tasks, ctx) [default: no-op]
* registerPlannedTaskResults(tasks, ctx) [framework; per-task names from plannedTaskResultTypeNames() — implement]
* loop 1 — specification (guarded, strict-mode reporting): [framework]
* defs = analyse(task, ctx) [implement]
@@ -163,6 +165,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
PerformanceTracker tracker = context.getPerformanceTracker();
logger.infoTagged("PROCESSING ROUND START");
+ onRoundStart(context);
tracker.startPhase();
ElementGroups collected = findElements(annotations, roundEnv);
@@ -170,6 +173,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
logger.debugTagged("Processing round started.");
List> tasks = plan(collected, context);
+ onTasksPlanned(tasks, context);
if (tasks.isEmpty()) {
return claimsAnnotations(getSupportedAnnotationTypes());
}
@@ -522,7 +526,32 @@ protected void registerPlannedTaskResults(List> tasks, C context)
* @param generatedName the type name just registered for it
* @param context the processing context
*/
- protected void onTaskResultRegistered(CodegenTask task, TypeName generatedName, C context) {}
+ protected void onTaskResultRegistered(CodegenTask task, TypeName generatedName, C context) {
+ // Extension point for domains — registration bookkeeping is already done by the driver.
+ }
+
+ /**
+ * Event at the start of every processing round — the domain's hook for per-round preparation
+ * (resetting round state, round-specific logging). Called after the processing-over check, before
+ * element collection. Default: nothing.
+ *
+ * @param context the processing context
+ */
+ protected void onRoundStart(C context) {
+ // Extension point for domains — the driver performs no per-round setup beyond logging.
+ }
+
+ /**
+ * Event after the round's tasks were planned and their predicted results registered — the
+ * domain's hook to inspect the plan before analysis starts. Called every generating round with
+ * that round's task list, which may be empty when the round produced nothing. Default: nothing.
+ *
+ * @param tasks the tasks the domain's {@link #planTasks} produced this round
+ * @param context the processing context
+ */
+ protected void onTasksPlanned(List extends CodegenTask> tasks, C context) {
+ // Extension point for domains — planning bookkeeping is already done by the driver.
+ }
/**
* Whether the domain's generating round already ran. Default: one-shot — the base marks the
@@ -570,7 +599,9 @@ protected abstract GenerationTargetClassDto mapToTargetSpecification(
* @param generation the planned generation — task, analysed definition and generated target
* @param context the processing context
*/
- protected void onTaskGenerated(PlannedGeneration generation, C context) {}
+ protected void onTaskGenerated(PlannedGeneration generation, C context) {
+ // Extension point for domains — the target is already generated at this point.
+ }
/**
* Callback when processing is over — the domain's last-round finalization: SPI flush, reports,
@@ -578,10 +609,14 @@ protected void onTaskGenerated(PlannedGeneration generation, C context) {}
*
* @param context the processing context
*/
- protected void onProcessingOver(C context) {}
+ protected void onProcessingOver(C context) {
+ // Extension point for domains — e.g. last-round outputs via {@link #generateTarget}.
+ }
/** Callback at the end of a generating round — domain finalization. */
- protected void endRound(C context) {}
+ protected void endRound(C context) {
+ // Extension point for domains — e.g. per-round finalization.
+ }
/**
* One planned generation, carried from the analysis loop into the generation loop and handed to
From 8b0d6baa248a24e3cda77e2751dbb99e752989b9 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 17:22:11 +0000
Subject: [PATCH 078/170] processing: inline codeql dismissal on intentional
extension-point signatures
---
.../codegen/processing/AbstractCodegenProcessor.java | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index c2f2a842..88383284 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -412,6 +412,7 @@ private ProcessorOptionRegistry optionRegistry() {
* @param supportedTypes the supported annotation type names the round was processed for
* @return {@code true} to claim the supported annotations
*/
+ // codeql[java/unused-parameter] — extension point; domains may need every parameter.
protected boolean claimsAnnotations(Set supportedTypes) {
return false;
}
@@ -516,6 +517,7 @@ protected void registerPlannedTaskResults(List> tasks, C context)
* @param context the processing context
* @return the generated type names (empty when the task registers nothing)
*/
+ // codeql[java/unused-parameter] — extension point; domains may need every parameter.
protected abstract List plannedTaskResultTypeNames(CodegenTask task, C context);
/**
@@ -526,6 +528,7 @@ protected void registerPlannedTaskResults(List> tasks, C context)
* @param generatedName the type name just registered for it
* @param context the processing context
*/
+ // codeql[java/unused-parameter] — extension point; domains may need every parameter.
protected void onTaskResultRegistered(CodegenTask task, TypeName generatedName, C context) {
// Extension point for domains — registration bookkeeping is already done by the driver.
}
@@ -537,6 +540,7 @@ protected void onTaskResultRegistered(CodegenTask task, TypeName generatedNam
*
* @param context the processing context
*/
+ // codeql[java/unused-parameter] — extension point; domains may need every parameter.
protected void onRoundStart(C context) {
// Extension point for domains — the driver performs no per-round setup beyond logging.
}
@@ -549,6 +553,7 @@ protected void onRoundStart(C context) {
* @param tasks the tasks the domain's {@link #planTasks} produced this round
* @param context the processing context
*/
+ // codeql[java/unused-parameter] — extension point; domains may need every parameter.
protected void onTasksPlanned(List extends CodegenTask> tasks, C context) {
// Extension point for domains — planning bookkeeping is already done by the driver.
}
@@ -562,6 +567,7 @@ protected void onTasksPlanned(List extends CodegenTask> tasks, C context) {
* @param context the processing context
* @return {@code true} to skip this round
*/
+ // codeql[java/unused-parameter] — extension point; domains may need every parameter.
protected boolean isFinished(C context) {
return processedOnce;
}
@@ -599,6 +605,7 @@ protected abstract GenerationTargetClassDto mapToTargetSpecification(
* @param generation the planned generation — task, analysed definition and generated target
* @param context the processing context
*/
+ // codeql[java/unused-parameter] — extension point; domains may need every parameter.
protected void onTaskGenerated(PlannedGeneration generation, C context) {
// Extension point for domains — the target is already generated at this point.
}
@@ -609,11 +616,13 @@ protected void onTaskGenerated(PlannedGeneration generation, C context) {
*
* @param context the processing context
*/
+ // codeql[java/unused-parameter] — extension point; domains may need every parameter.
protected void onProcessingOver(C context) {
// Extension point for domains — e.g. last-round outputs via {@link #generateTarget}.
}
/** Callback at the end of a generating round — domain finalization. */
+ // codeql[java/unused-parameter] — extension point; domains may need every parameter.
protected void endRound(C context) {
// Extension point for domains — e.g. per-round finalization.
}
From 3504a9b540732f7c9277d292cca87633c495db72 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 20:58:46 +0000
Subject: [PATCH 079/170] docs: performance report phase names follow the
driver's literals
---
docs/CONFIGURATION.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index dc187388..65afe043 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -1215,7 +1215,7 @@ processing. A summary report is logged to the compiler output at the end of proc
**When enabled**: A hierarchical performance report is printed to the compiler log, including:
- Total processing time and average time per class
-- Phase breakdown (Element Collection, Configuration Resolution, Builder Definition Extraction, DTO Mapping, Code Generation)
+- Phase breakdown (Element Collection, Configuration Resolution, Analysis, Target Mapping, Code Generation)
- Top 20 slowest classes with field and collection counts
- Top 5 slowest MethodGenerators and BuilderEnhancers
From 1b3cf597c07d848226330fb323d629ca30224520 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 21:12:00 +0000
Subject: [PATCH 080/170] =?UTF-8?q?processing:=20javadoc=20leftovers=20?=
=?UTF-8?q?=E2=80=94=20stale=20links,=20wrong=20param=20names,=20missing?=
=?UTF-8?q?=20tags?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AbstractCodegenProcessor.java | 29 ++++++++++++++-----
.../CodegenProcessorConfiguration.java | 4 +--
.../codegen/processing/ElementGroups.java | 2 +-
3 files changed, 25 insertions(+), 10 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 88383284..7bb6af25 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -50,10 +50,11 @@
* the domain plugs in its hooks — collect, plan, register, analyse/map, generate, finalize.
*
* A new processor declares its static surface once as {@link CodegenProcessorConfiguration}
- * (passed to the constructor — element spec, options, source level) and implements only the domain
- * hooks marked {@code [implement]}: {@link #createContext}, {@link #planTasks}, {@link
- * #plannedTaskResultTypeNames}, {@link #analyse} and {@link #mapToTargetSpecification}. Every other
- * step is framework-owned or an optional {@code [default]} the domain may override.
+ * (passed to the constructor — element collector, options, source level, formatter factory) and
+ * implements only the domain hooks marked {@code [implement]}: {@link #createContext}, {@link
+ * #planTasks}, {@link #plannedTaskResultTypeNames}, {@link #analyse} and {@link
+ * #mapToTargetSpecification}. Every other step is framework-owned or an optional {@code [default]}
+ * the domain may override.
*
*
The round spine:
*
@@ -262,8 +263,9 @@ public final Set getSupportedAnnotationTypes() {
/**
* The supported annotation type names declared by the {@link SupportedAnnotationTypes} annotation
- * on the processor class. The element spec's annotation types are for collection filtering — not
- * the supported-set declaration — so without the annotation the supported set stays {@code "*"}.
+ * on the processor class. The element collector's annotation types are for collection filtering —
+ * not the supported-set declaration — so without the annotation the supported set stays {@code
+ * "*"}.
*
* @return the declared names, or empty when the class is not annotated
*/
@@ -365,6 +367,11 @@ protected abstract C createContext(
/**
* Creates the code generation engine — the {@link ClassCodeGenerator} backend port the domain
* wires over {@link #generationEnvironment}.
+ *
+ * @param processingEnv the processing environment
+ * @param logger the processing logger
+ * @param context the processing context
+ * @return the domain's code generation engine
*/
protected abstract ClassCodeGenerator createGenerator(
ProcessingEnvironment processingEnv, ProcessingLogger logger, C context);
@@ -387,6 +394,10 @@ protected GenerationEnvironment generationEnvironment(
/**
* Creates the source formatter resolver for the configured profile — profile name and formatter
* factory come from the processor's declared options and configuration.
+ *
+ * @param reader the compiler arguments reader
+ * @param logger the processing logger
+ * @return the formatter resolver
*/
protected SourceFormatterResolver createFormatterResolver(
CompilerArgumentsReader reader, ProcessingLogger logger) {
@@ -621,7 +632,11 @@ protected void onProcessingOver(C context) {
// Extension point for domains — e.g. last-round outputs via {@link #generateTarget}.
}
- /** Callback at the end of a generating round — domain finalization. */
+ /**
+ * Callback at the end of a generating round — domain finalization.
+ *
+ * @param context the processing context
+ */
// codeql[java/unused-parameter] — extension point; domains may need every parameter.
protected void endRound(C context) {
// Extension point for domains — e.g. per-round finalization.
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
index a893bdd3..f352a4a4 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
@@ -48,8 +48,8 @@ public record CodegenProcessorConfiguration(
SourceFormatterResolver.FormatterFactory formatterFactory) {
/**
- * Creates a builder with all defaults applied — only {@link #elementCollector(ElementCollector)}
- * must be set.
+ * Creates a builder with all defaults applied — only {@link
+ * Builder#elementCollector(ElementCollector)} must be set.
*
* @return a new builder
*/
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementGroups.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementGroups.java
index 84d7d826..91b990c0 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementGroups.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementGroups.java
@@ -43,7 +43,7 @@ public final class ElementGroups {
/**
* The elements selected by the named selector, sorted by simple name.
*
- * @param selectorName name of an {@link ElementSelector}
+ * @param groupName name of the selecting {@link ElementSelector}'s group
* @return the selected elements, or an empty list when the selector matched nothing
*/
public List byGroup(String groupName) {
From fe76011ed3dd5fed469d4bcbd1366f22ae65f7e2 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 21:27:37 +0000
Subject: [PATCH 081/170] =?UTF-8?q?processing:=20tag=20the=20last=20untagg?=
=?UTF-8?q?ed=20driver=20messages=20=E2=80=94=20error=20via=20prefixed(),?=
=?UTF-8?q?=20debug=20via=20debugTagged?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/AbstractCodegenProcessor.java | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 7bb6af25..e4ae7c21 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -140,8 +140,11 @@ private boolean verifySupportedJdk() {
if (!supported) {
String releaseNumber = requiredSourceVersion().replace("RELEASE_", "");
context.error(
- "%s requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor.",
- logger.getTag(), releaseNumber, current, releaseNumber);
+ logger.prefixed(
+ "requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor."),
+ releaseNumber,
+ current,
+ releaseNumber);
}
return supported;
}
@@ -441,7 +444,7 @@ private boolean isAtLeast(SourceVersion current, String requiredName) {
try {
return current.ordinal() >= SourceVersion.valueOf(requiredName).ordinal();
} catch (IllegalArgumentException ex) {
- logger.debug(
+ logger.debugTagged(
"Required source version name '%s' is unknown to this JDK — treating as unsupported",
requiredName);
return false;
From 1c989aaf68900c58305b2499b0a90b0498370b1f Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 21:40:06 +0000
Subject: [PATCH 082/170] =?UTF-8?q?logging:=20tag=20every=20emitted=20mess?=
=?UTF-8?q?age=20inside=20the=20logger=20=E2=80=94=20drop=20the=20tagged/u?=
=?UTF-8?q?ntagged=20API=20split?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/CONTRIBUTING.md | 24 ++--
docs/DEBUG_LOGGING.md | 70 +++++-----
.../processing/AbstractCodegenProcessor.java | 27 ++--
.../builders/processor/BuilderProcessor.java | 30 ++---
.../integration/JacksonModuleGenerator.java | 6 +-
.../logging/ActivePerformanceTracker.java | 2 +-
.../processing/logging/ProcessingLogger.java | 81 +++---------
.../processor/BuilderProcessorTest.java | 124 +++++++++---------
8 files changed, 156 insertions(+), 208 deletions(-)
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 0cfd6789..6c5448e0 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -243,18 +243,18 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md).
[DEBUG] simple-builders: Found 1 annotated elements.
[DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor.
[DEBUG] simple-builders: 1 of 1 annotated element(s) are inside the builderGenerationPackages scope.
-[DEBUG] Processing element: Project
-[DEBUG] ├─ Extracting builder definition from: test.Project
-[DEBUG] │ ├─ Builder will be generated as: test.ProjectBuilder
-[DEBUG] │ ├─ Analysing setters for finding fields
-[DEBUG] │ │ ├─ Analyzing method: setName(java.lang.String)
-[DEBUG] │ │ │ └─ Adding field: name (type: java.lang.String)
-[DEBUG] │ └─ Processed 1 possible setters: added 1 fields, skipped 0
-[DEBUG] Generating targets for: Project
-[DEBUG] ├─ Code generation for builder: ProjectBuilder
-[DEBUG] │ ├─ Adding Methods for 4 candidates
-[DEBUG] │ │ └─ 4 Methods added
-[DEBUG] │ └─ Successfully generated builder: ProjectBuilder
+[DEBUG] simple-builders: Processing element: Project
+[DEBUG] ├─ simple-builders: Extracting builder definition from: test.Project
+[DEBUG] │ ├─ simple-builders: Builder will be generated as: test.ProjectBuilder
+[DEBUG] │ ├─ simple-builders: Analysing setters for finding fields
+[DEBUG] │ │ ├─ simple-builders: Analyzing method: setName(java.lang.String)
+[DEBUG] │ │ │ └─ simple-builders: Adding field: name (type: java.lang.String)
+[DEBUG] │ └─ simple-builders: Processed 1 possible setters: added 1 fields, skipped 0
+[DEBUG] simple-builders: Generating targets for: Project
+[DEBUG] ├─ simple-builders: Code generation for builder: ProjectBuilder
+[DEBUG] │ ├─ simple-builders: Adding Methods for 4 candidates
+[DEBUG] │ │ └─ simple-builders: 4 Methods added
+[DEBUG] │ └─ simple-builders: Successfully generated builder: ProjectBuilder
simple-builders: Successfully generated 1 target(s) in this processing round
=============================================
diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md
index b302e0e2..97360650 100644
--- a/docs/DEBUG_LOGGING.md
+++ b/docs/DEBUG_LOGGING.md
@@ -80,7 +80,7 @@ The Simple Builders processor provides **conditional hierarchical logging** to b
- Provides full visibility into the processing hierarchy
- Example: `[INFO] │ └─ simple-builders: Successfully generated 3 target(s) in this processing round`
- Example: `[WARNING] │ │ ├─ Builder field conflict: field 'name'...`
-- Example: `[INFO] [DEBUG] │ │ │ └─ Processing method: setName`
+- Example: `[INFO] [DEBUG] │ │ │ └─ simple-builders: Processing method: setName`
- All message types align vertically despite different prefix lengths
This approach ensures clean production logs while maintaining full debugging capabilities when needed.
@@ -95,40 +95,40 @@ When debug logging is enabled, you'll see detailed output with visual separators
[INFO] [DEBUG] simple-builders: Found 3 annotated elements.
[INFO] [DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor.
[INFO] [DEBUG] simple-builders: 3 of 3 annotated element(s) are inside the builderGenerationPackages scope.
-[INFO] [DEBUG] Processing element: PersonDto
-[INFO] [DEBUG] ├─ Extracting builder definition from: org.example.PersonDto
-[INFO] [DEBUG] │ ├─ Builder will be generated as: org.example.PersonDtoBuilder
-[INFO] [DEBUG] │ ├─ Analysing setters for finding fields
-[INFO] [DEBUG] │ │ ├─ Analyzing method: setName(java.lang.String)
-[INFO] [DEBUG] │ │ │ └─ Adding field: name (type: java.lang.String)
-[INFO] [DEBUG] │ │ ├─ Analyzing method: setAge(int)
-[INFO] [DEBUG] │ │ │ └─ Adding field: age (type: int)
-[INFO] [DEBUG] │ └─ Processed 2 possible setters: added 2 fields, skipped 0
-[INFO] [DEBUG] Processing element: OrderDto
-[INFO] [DEBUG] ├─ Extracting builder definition from: org.example.OrderDto
-[INFO] [DEBUG] │ └─ Builder will be generated as: org.example.OrderDtoBuilder
-[INFO] [DEBUG] │ └─ Processed 1 possible setters: added 1 fields, skipped 0
-[INFO] [DEBUG] Processing element: CustomerDto (with conflicts)
-[INFO] [DEBUG] ├─ Extracting builder definition from: org.example.CustomerDto
-[INFO] [DEBUG] │ └─ Builder will be generated as: org.example.CustomerDtoBuilder
-[INFO] [DEBUG] │ └─ Processed 2 possible setters: added 2 fields, skipped 0
-[WARNING] │ │ └─ Builder field conflict: field 'name' (type Optional) renamed to 'nameOptional' to avoid conflict
-[INFO] [DEBUG] Generating targets for: PersonDto
-[INFO] [DEBUG] ├─ Code generation for builder: PersonDtoBuilder
-[INFO] [DEBUG] │ ├─ Class builder created
-[INFO] [DEBUG] │ ├─ Generating 0 constructor fields and 2 setter fields
-[INFO] [DEBUG] │ │ └─ Fields added: 2 fields
-[INFO] [DEBUG] │ ├─ Adding Methods for 4 candidates
-[INFO] [DEBUG] │ │ └─ 4 Methods added
-[INFO] [DEBUG] │ ├─ Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'.
-[INFO] [DEBUG] │ ├─ Writing builder class to file: org.example.PersonDtoBuilder
-[INFO] [DEBUG] │ └─ Successfully generated builder: PersonDtoBuilder
-[INFO] [DEBUG] Generating targets for: OrderDto
-[INFO] [DEBUG] ├─ Code generation for builder: OrderDtoBuilder
-[INFO] [DEBUG] │ └─ Successfully generated builder: OrderDtoBuilder
-[INFO] [DEBUG] Generating targets for: CustomerDto
-[INFO] [DEBUG] ├─ Code generation for builder: CustomerDtoBuilder
-[INFO] [DEBUG] │ └─ Successfully generated builder: CustomerDtoBuilder
+[INFO] [DEBUG] simple-builders: Processing element: PersonDto
+[INFO] [DEBUG] ├─ simple-builders: Extracting builder definition from: org.example.PersonDto
+[INFO] [DEBUG] │ ├─ simple-builders: Builder will be generated as: org.example.PersonDtoBuilder
+[INFO] [DEBUG] │ ├─ simple-builders: Analysing setters for finding fields
+[INFO] [DEBUG] │ │ ├─ simple-builders: Analyzing method: setName(java.lang.String)
+[INFO] [DEBUG] │ │ │ └─ simple-builders: Adding field: name (type: java.lang.String)
+[INFO] [DEBUG] │ │ ├─ simple-builders: Analyzing method: setAge(int)
+[INFO] [DEBUG] │ │ │ └─ simple-builders: Adding field: age (type: int)
+[INFO] [DEBUG] │ └─ simple-builders: Processed 2 possible setters: added 2 fields, skipped 0
+[INFO] [DEBUG] simple-builders: Processing element: OrderDto
+[INFO] [DEBUG] ├─ simple-builders: Extracting builder definition from: org.example.OrderDto
+[INFO] [DEBUG] │ └─ simple-builders: Builder will be generated as: org.example.OrderDtoBuilder
+[INFO] [DEBUG] │ └─ simple-builders: Processed 1 possible setters: added 1 fields, skipped 0
+[INFO] [DEBUG] simple-builders: Processing element: CustomerDto (with conflicts)
+[INFO] [DEBUG] ├─ simple-builders: Extracting builder definition from: org.example.CustomerDto
+[INFO] [DEBUG] │ └─ simple-builders: Builder will be generated as: org.example.CustomerDtoBuilder
+[INFO] [DEBUG] │ └─ simple-builders: Processed 2 possible setters: added 2 fields, skipped 0
+[WARNING] │ │ └─ simple-builders: Builder field conflict: field 'name' (type Optional) renamed to 'nameOptional' to avoid conflict
+[INFO] [DEBUG] simple-builders: Generating targets for: PersonDto
+[INFO] [DEBUG] ├─ simple-builders: Code generation for builder: PersonDtoBuilder
+[INFO] [DEBUG] │ ├─ simple-builders: Class builder created
+[INFO] [DEBUG] │ ├─ simple-builders: Generating 0 constructor fields and 2 setter fields
+[INFO] [DEBUG] │ │ └─ simple-builders: Fields added: 2 fields
+[INFO] [DEBUG] │ ├─ simple-builders: Adding Methods for 4 candidates
+[INFO] [DEBUG] │ │ └─ simple-builders: 4 Methods added
+[INFO] [DEBUG] │ ├─ simple-builders: Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'.
+[INFO] [DEBUG] │ ├─ simple-builders: Writing builder class to file: org.example.PersonDtoBuilder
+[INFO] [DEBUG] │ └─ simple-builders: Successfully generated builder: PersonDtoBuilder
+[INFO] [DEBUG] simple-builders: Generating targets for: OrderDto
+[INFO] [DEBUG] ├─ simple-builders: Code generation for builder: OrderDtoBuilder
+[INFO] [DEBUG] │ └─ simple-builders: Successfully generated builder: OrderDtoBuilder
+[INFO] [DEBUG] simple-builders: Generating targets for: CustomerDto
+[INFO] [DEBUG] ├─ simple-builders: Code generation for builder: CustomerDtoBuilder
+[INFO] [DEBUG] │ └─ simple-builders: Successfully generated builder: CustomerDtoBuilder
[INFO] simple-builders: Successfully generated 3 target(s) in this processing round
[INFO] simple-builders: PROCESSING ROUND START
[INFO] [DEBUG] simple-builders: Processing round started.
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index e4ae7c21..0f146c4c 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -140,11 +140,8 @@ private boolean verifySupportedJdk() {
if (!supported) {
String releaseNumber = requiredSourceVersion().replace("RELEASE_", "");
context.error(
- logger.prefixed(
- "requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor."),
- releaseNumber,
- current,
- releaseNumber);
+ "requires Java %s or higher for annotation processing. Detected: '%s'. Please upgrade to JDK %s+ or disable the processor.",
+ releaseNumber, current, releaseNumber);
}
return supported;
}
@@ -168,13 +165,13 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
}
PerformanceTracker tracker = context.getPerformanceTracker();
- logger.infoTagged("PROCESSING ROUND START");
+ logger.info("PROCESSING ROUND START");
onRoundStart(context);
tracker.startPhase();
ElementGroups collected = findElements(annotations, roundEnv);
tracker.endPhase("Element Collection");
- logger.debugTagged("Processing round started.");
+ logger.debug("Processing round started.");
List> tasks = plan(collected, context);
onTasksPlanned(tasks, context);
@@ -211,9 +208,7 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
// By default generation failures are warnings so other targets are still generated.
// In opt-in strict mode they are promoted to errors that fail the build.
context.reportBasedOnStrictMode(
- task.reportingElement(),
- logger.prefixed("Failed to generate target - %s"),
- ex.getMessage());
+ task.reportingElement(), "Failed to generate target - %s", ex.getMessage());
} finally {
context.debugEndOperation();
}
@@ -235,16 +230,14 @@ public final boolean process(Set extends TypeElement> annotations, RoundEnviro
successfulGenerations++;
} catch (Exception ex) {
context.reportBasedOnStrictMode(
- work.task().reportingElement(),
- logger.prefixed("Failed to generate target - %s"),
- ex.getMessage());
+ work.task().reportingElement(), "Failed to generate target - %s", ex.getMessage());
} finally {
context.debugEndOperation();
}
}
if (successfulGenerations > 0) {
- logger.infoTagged(
+ logger.info(
"Successfully generated %d target(s) in this processing round", successfulGenerations);
}
@@ -331,8 +324,8 @@ private void assertTargetDoesNotExist(GenerationTargetClassDto target)
}
/**
- * The diagnostic prefix the processor's tagged messages carry — see {@link
- * ProcessingLogger#prefixed}. Default: the processor's simple class name; override for a shorter
+ * The diagnostic prefix carried by every emitted log line ("{@code prefix: }" — applied by the
+ * logger to each message). Default: the processor's simple class name; override for a shorter
* domain prefix.
*
* @return the logger prefix
@@ -444,7 +437,7 @@ private boolean isAtLeast(SourceVersion current, String requiredName) {
try {
return current.ordinal() >= SourceVersion.valueOf(requiredName).ordinal();
} catch (IllegalArgumentException ex) {
- logger.debugTagged(
+ logger.debug(
"Required source version name '%s' is unknown to this JDK — treating as unsupported",
requiredName);
return false;
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 21cce7ef..911b64e6 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -178,10 +178,8 @@ protected List> planTasks(
ElementGroups collected, BuilderProcessingContext ctx) {
List sortedElements = collected.byGroup(GROUP_ANNOTATED_ELEMENTS);
List sortedHolders = collected.byGroup(GROUP_EXTERNAL_TYPE_HOLDERS);
- ctx.debug("simple-builders: Found %d annotated elements.", sortedElements.size());
- ctx.debug(
- "simple-builders: Found %d type(s) for generation with @SimpleBuilderFor.",
- sortedHolders.size());
+ ctx.debug("Found %d annotated elements.", sortedElements.size());
+ ctx.debug("Found %d type(s) for generation with @SimpleBuilderFor.", sortedHolders.size());
List> tasks =
resolveGenerationPlan(
sortedElements,
@@ -189,7 +187,7 @@ protected List> planTasks(
ctx.getConfigurationReader(),
ctx.getPerformanceTracker());
ctx.debug(
- "simple-builders: %d of %d annotated element(s) are inside the builderGenerationPackages scope.",
+ "%d of %d annotated element(s) are inside the builderGenerationPackages scope.",
tasks.size(), sortedElements.size());
return tasks;
}
@@ -227,7 +225,7 @@ private List> resolveGenerationPlan(
// By default builder generation failures are warnings so other builders are still
// generated. In opt-in strict mode they are promoted to errors that fail the build.
context.reportBasedOnStrictMode(
- annotatedElement, "simple-builders: Failed to generate builder - %s", ex.getMessage());
+ annotatedElement, "Failed to generate builder - %s", ex.getMessage());
} finally {
context.debugEndOperation();
}
@@ -238,8 +236,7 @@ private List> resolveGenerationPlan(
try {
tasks.addAll(planGenerationOfTypeByHolder(holder, reader, alreadyPlannedBuilders, tracker));
} catch (BuilderException ex) {
- context.reportBasedOnStrictMode(
- holder, "simple-builders: Failed to generate builder - %s", ex.getMessage());
+ context.reportBasedOnStrictMode(holder, "Failed to generate builder - %s", ex.getMessage());
} finally {
context.debugEndOperation();
}
@@ -294,7 +291,7 @@ private List> planGenerationOfTypeByHolder(
if (targets.isEmpty()) {
context.warning(
holder,
- "simple-builders: @SimpleBuilderFor on '%s' resolves no types - nothing to generate",
+ "@SimpleBuilderFor on '%s' resolves no types - nothing to generate",
holder.getSimpleName());
continue;
}
@@ -329,7 +326,7 @@ private Optional> planExternalTarget(
&& !config.builderGenerationPackages().includes(builderPackage)) {
context.warning(
holder,
- "simple-builders: @SimpleBuilderFor on '%s' generates builder for '%s' in package '%s', which is outside builderGenerationPackages - the explicit declaration takes precedence",
+ "@SimpleBuilderFor on '%s' generates builder for '%s' in package '%s', which is outside builderGenerationPackages - the explicit declaration takes precedence",
holder.getSimpleName(),
target.getQualifiedName(),
builderPackage);
@@ -338,7 +335,7 @@ private Optional> planExternalTarget(
if (!alreadyPlannedBuilders.add(builderName)) {
context.warning(
holder,
- "simple-builders: skipping '%s' declared in @SimpleBuilderFor on '%s' - builder '%s' is already generated elsewhere",
+ "skipping '%s' declared in @SimpleBuilderFor on '%s' - builder '%s' is already generated elsewhere",
target.getQualifiedName(),
holder.getSimpleName(),
builderName.getFullQualifiedName());
@@ -394,7 +391,7 @@ private List findPackageTargetTypes(Element holder, String packageN
if (packageElement == null) {
context.warning(
holder,
- "simple-builders: package '%s' in @SimpleBuilderFor on '%s' could not be resolved - skipped",
+ "package '%s' in @SimpleBuilderFor on '%s' could not be resolved - skipped",
packageName,
holder.getSimpleName());
return List.of();
@@ -405,7 +402,7 @@ private List findPackageTargetTypes(Element holder, String packageN
targets.add(type);
} else {
context.debug(
- "simple-builders: skipping %s '%s' in package '%s' declared by @SimpleBuilderFor on '%s' - no builder can be generated",
+ "skipping %s '%s' in package '%s' declared by @SimpleBuilderFor on '%s' - no builder can be generated",
type.getKind().toString().toLowerCase(),
type.getQualifiedName(),
packageName,
@@ -535,15 +532,14 @@ protected void onProcessingOver(BuilderProcessingContext ctx) {
for (GenerationTargetClassDto moduleDefinition :
jacksonModuleGenerator.getModuleDefinitions()) {
ctx.info(
- "simple-builders: Generating Jackson module in package '%s'",
+ "Generating Jackson module in package '%s'",
moduleDefinition.getTypeName().getPackageName());
try {
generateTarget(moduleDefinition);
} catch (Exception e) {
ctx.reportBasedOnStrictMode(
- logger.prefixed("Error generating Jackson module for package %s: %s"),
- moduleDefinition.getTypeName().getPackageName(),
- e.getMessage());
+ "Error generating Jackson module for package %s: %s",
+ moduleDefinition.getTypeName().getPackageName(), e.getMessage());
}
}
// Reset indentation after Jackson module generation as well
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java
index f2642b2b..4e732f29 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java
@@ -112,7 +112,7 @@ private boolean validateForModuleGeneration(BuilderConfiguration config, Element
if (!jacksonAvailable) {
logger.warning(
sourceElement,
- "simple-builders: generateJacksonModule is enabled for %s, but Jackson dependencies (com.fasterxml.jackson.databind.module.SimpleModule) are not found on the classpath. Module generation skipped.",
+ "generateJacksonModule is enabled for %s, but Jackson dependencies (com.fasterxml.jackson.databind.module.SimpleModule) are not found on the classpath. Module generation skipped.",
sourceElement.getSimpleName());
return false;
}
@@ -120,7 +120,7 @@ private boolean validateForModuleGeneration(BuilderConfiguration config, Element
if (!config.shouldUseJacksonDeserializerAnnotation()) {
logger.warning(
sourceElement,
- "simple-builders: generateJacksonModule is enabled but usingJacksonDeserializerAnnotation is disabled. "
+ "generateJacksonModule is enabled but usingJacksonDeserializerAnnotation is disabled. "
+ "This is a misconfiguration. Jackson Module will NOT be generated for %s.",
sourceElement.getSimpleName());
return false;
@@ -143,7 +143,7 @@ public List getModuleDefinitions() {
List definitions = new ArrayList<>();
if (!entriesByPackage.isEmpty()) {
logger.info(
- "simple-builders: Processing OVER. JacksonModuleEnabled: true, Packages: %d",
+ "Processing OVER. JacksonModuleEnabled: true, Packages: %d",
entriesByPackage.keySet().size());
for (String packageName : entriesByPackage.keySet()) {
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTracker.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTracker.java
index bc0759d8..b3636e7e 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTracker.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTracker.java
@@ -182,7 +182,7 @@ public void generateReport(ProcessingLogger logger) {
long totalTime = System.nanoTime() - totalStartTime;
double totalSeconds = totalTime / 1_000_000_000.0;
- logger.info("simple-builders: PERFORMANCE REPORT");
+ logger.info("PERFORMANCE REPORT");
logger.info("================================");
logger.info("Total classes processed: %d", totalClasses.get());
logger.info(String.format(Locale.US, "Total processing time: %.1fs", totalSeconds));
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
index 717bbc99..4da738a2 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
@@ -32,7 +32,8 @@
/**
* Logger for all messages during annotation processing. Providing util-functions for posting
* messages easily. Using a {@code javax.annotation.processing.Messager} inside to publish the
- * messages.
+ * messages. Every emitted message is prefixed with the logger's diagnostic tag ("{@code tag: }")
+ * when one was supplied at creation.
*/
public class ProcessingLogger {
/** Util class for exposing messages of type {@code javax.annotation.processing.Messager}. */
@@ -41,10 +42,7 @@ public class ProcessingLogger {
/** Verbosity level of this logger. */
private final Verbosity verbosity;
- /**
- * Diagnostic tag identifying the processor — domains read it back via {@link #getTag()} to prefix
- * their own messages.
- */
+ /** Diagnostic tag identifying the processor — every emitted message is prefixed with it. */
private final String tag;
/** Thread-local indentation level for hierarchical logging. */
@@ -112,54 +110,14 @@ private ProcessingLogger(ProcessingEnvironment processingEnv, Verbosity verbosit
}
/**
- * The diagnostic tag this logger was created with (typically the processor's name or a shorter
- * domain tag), or {@code null}.
- *
- * @return the tag
- */
- public String getTag() {
- return tag;
- }
-
- /**
- * Applies this logger's "{@code tag: }" prefix to a message format — the one place the tag-prefix
- * pattern is defined. Untagged loggers return the format unchanged.
- *
- * @param format the message format
- * @return the format prefixed with this logger's tag
- */
- public String prefixed(String format) {
- return tag != null ? tag + ": " + format : format;
- }
-
- /**
- * Logs an error message prefixed with this logger's tag.
+ * Applies this logger's "{@code tag: }" prefix to a message — the one place the tag-prefix
+ * pattern is defined. Untagged loggers return the message unchanged.
*
- * @param format the message format
- * @param args the format arguments
+ * @param message the message or message format
+ * @return the message prefixed with this logger's tag
*/
- public void errorTagged(String format, Object... args) {
- error(prefixed(format), args);
- }
-
- /**
- * Logs an info message prefixed with this logger's tag.
- *
- * @param format the message format
- * @param args the format arguments
- */
- public void infoTagged(String format, Object... args) {
- info(prefixed(format), args);
- }
-
- /**
- * Logs a debug message prefixed with this logger's tag.
- *
- * @param format the message format
- * @param args the format arguments
- */
- public void debugTagged(String format, Object... args) {
- debug(prefixed(format), args);
+ private String prefixed(String message) {
+ return tag != null ? tag + ": " + message : message;
}
/** Resets ThreadLocal state to ensure clean state between test runs. */
@@ -174,7 +132,7 @@ private void resetThreadLocalState() {
* @param args arguments referenced by the format specifiers in the format string
*/
public void error(String format, Object... args) {
- messager.printMessage(Diagnostic.Kind.ERROR, String.format(format, args));
+ messager.printMessage(Diagnostic.Kind.ERROR, String.format(prefixed(format), args));
}
/**
@@ -185,7 +143,7 @@ public void error(String format, Object... args) {
* @param args arguments referenced by the format specifiers in the format string
*/
public void error(Element e, String format, Object... args) {
- messager.printMessage(Diagnostic.Kind.ERROR, String.format(format, args), e);
+ messager.printMessage(Diagnostic.Kind.ERROR, String.format(prefixed(format), args), e);
}
/**
@@ -195,7 +153,7 @@ public void error(Element e, String format, Object... args) {
* @param message the info message to be posted
*/
public void info(String message) {
- messager.printMessage(Diagnostic.Kind.NOTE, message);
+ messager.printMessage(Diagnostic.Kind.NOTE, prefixed(message));
}
/**
@@ -219,7 +177,7 @@ private String formatHierarchicalMessage(String message, int spaces) {
* @param args arguments referenced by the format specifiers in the format string
*/
public void info(String format, Object... args) {
- String message = String.format(format, args);
+ String message = String.format(prefixed(format), args);
if (verbosity == Verbosity.VERBOSE) {
// When debug is enabled, add spaces to align with [DEBUG] prefix (which is 6 characters
// longer)
@@ -240,7 +198,7 @@ public void info(String format, Object... args) {
*/
public void debug(String message) {
if (verbosity == Verbosity.VERBOSE) {
- String indentedMessage = formatWithIndentation(message);
+ String indentedMessage = formatWithIndentation(prefixed(message));
messager.printMessage(Diagnostic.Kind.OTHER, indentedMessage);
}
}
@@ -254,7 +212,7 @@ public void debug(String message) {
*/
public void debug(String format, Object... args) {
if (verbosity == Verbosity.VERBOSE) {
- String message = String.format(format, args);
+ String message = String.format(prefixed(format), args);
String indentedMessage = formatWithIndentation(message);
messager.printMessage(Diagnostic.Kind.OTHER, indentedMessage);
}
@@ -268,7 +226,7 @@ public void debug(String format, Object... args) {
* @param args arguments referenced by the format specifiers in the format string
*/
public void warning(String format, Object... args) {
- String message = String.format(format, args);
+ String message = String.format(prefixed(format), args);
if (verbosity == Verbosity.VERBOSE) {
// When debug is enabled, add spaces to align with [DEBUG] prefix (which is 5 characters
// longer)
@@ -288,7 +246,7 @@ public void warning(String format, Object... args) {
* @param args arguments referenced by the format specifiers in the format string
*/
public void warning(Element e, String format, Object... args) {
- String message = String.format(format, args);
+ String message = String.format(prefixed(format), args);
if (verbosity == Verbosity.VERBOSE) {
// When debug is enabled, add spaces to align with [DEBUG] prefix (which is 5 characters
// longer)
@@ -370,7 +328,8 @@ public void debugStartOperation(String formatClosingMessage, Object... args) {
// Log the operation start message with proper prefix handling
if (verbosity == Verbosity.VERBOSE) {
- String operationMessage = formatWithIndentation(String.format(formatClosingMessage, args));
+ String operationMessage =
+ formatWithIndentation(String.format(prefixed(formatClosingMessage), args));
messager.printMessage(Diagnostic.Kind.NOTE, operationMessage);
}
@@ -403,7 +362,7 @@ public void debugEndOperation(String formatClosingMessage, Object... args) {
// Log the closing message with └─ to indicate it's the last operation at this level
if (verbosity == Verbosity.VERBOSE) {
- String closingMessage = String.format(formatClosingMessage, args);
+ String closingMessage = String.format(prefixed(formatClosingMessage), args);
String operationMessage = formatWithIndentation(closingMessage, "└─ ");
messager.printMessage(Diagnostic.Kind.NOTE, operationMessage);
}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
index b61df4f5..53ca6b59 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
@@ -91,78 +91,78 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() {
ProcessorAsserts.assertNotesInOrder(
compilation,
// Processor init
- "[DEBUG] Starting BuilderProcessor...",
- "[DEBUG] Loaded global configuration from compiler arguments: BuilderConfiguration[verbose=ENABLED]",
- "[DEBUG] Initializing generator registry",
- "[DEBUG] ├─ Loaded 15 method generators and 10 builder enhancers total",
- "[DEBUG] └─ Initialized GeneratorRegistry with 15 method generators and 10 builder",
+ "[DEBUG] simple-builders: Starting BuilderProcessor...",
+ "[DEBUG] simple-builders: Loaded global configuration from compiler arguments: BuilderConfiguration[verbose=ENABLED]",
+ "[DEBUG] simple-builders: Initializing generator registry",
+ "[DEBUG] ├─ simple-builders: Loaded 15 method generators and 10 builder enhancers total",
+ "[DEBUG] └─ simple-builders: Initialized GeneratorRegistry with 15 method generators and 10 builder",
// Round 1 — start
"simple-builders: PROCESSING ROUND START",
"[DEBUG] simple-builders: Processing round started.",
"[DEBUG] simple-builders: Found 1 annotated elements.",
"[DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor.",
// Round 1 — configuration resolution
- "[DEBUG] Processing element: VerboseTest",
- "[DEBUG] ├─ Resolving configuration for element: VerboseTest",
- "[DEBUG] │ ├─ Built-in template @SimpleBuilder found in DIRECT scope",
- "[DEBUG] │ └─ Resulting configuration resolved: BuilderConfiguration[",
+ "[DEBUG] simple-builders: Processing element: VerboseTest",
+ "[DEBUG] ├─ simple-builders: Resolving configuration for element: VerboseTest",
+ "[DEBUG] │ ├─ simple-builders: Built-in template @SimpleBuilder found in DIRECT scope",
+ "[DEBUG] │ └─ simple-builders: Resulting configuration resolved: BuilderConfiguration[",
"[DEBUG] simple-builders: 1 of 1 annotated element(s) are inside the"
+ " builderGenerationPackages scope.",
// Round 1 — builder definition extraction
- "[DEBUG] Processing element: VerboseTest",
- "[DEBUG] ├─ Extracting builder definition from: test.VerboseTest",
- "[DEBUG] │ ├─ Builder will be generated as: test.VerboseTestBuilder",
- "[DEBUG] │ ├─ Analysing setters for finding fields",
- "[DEBUG] │ │ ├─ Analyzing method: setName(java.lang.String)",
- "[DEBUG] │ │ │ ├─ Processing method generators",
- "[DEBUG] │ │ │ │ ├─ Applying: BasicSetterGenerator (priority: 100)",
- "[DEBUG] │ │ │ │ ├─ Applying: StringFormatHelperGenerator (priority: 80)",
- "[DEBUG] │ │ │ │ ├─ Applying: SupplierMethodGenerator (priority: 60)",
- "[DEBUG] │ │ │ │ ├─ Applying: StringBuilderConsumerGenerator (priority: 45)",
- "[DEBUG] │ │ │ │ ├─ Applying: UpdateHelperGenerator (priority: 31)",
- "[DEBUG] │ │ │ │ └─ Generated 5 methods",
- "[DEBUG] │ │ │ └─ Adding field: name (type: java.lang.String)",
- "[DEBUG] │ │ └─ Processed 1 possible setters: added 1 fields, skipped 0",
- "[DEBUG] │ ├─ Processing class based enhancer",
- "[DEBUG] │ │ ├─ Applying: GeneratedAnnotationEnhancer (priority: 120)",
- "[DEBUG] │ │ ├─ Applying: BuilderImplementationAnnotationEnhancer (priority: 115)",
- "[DEBUG] │ │ ├─ Added @BuilderImplementation annotation to builder VerboseTestBuilder",
- "[DEBUG] │ │ ├─ Applying: CoreMethodsEnhancer (priority: 100)",
- "[DEBUG] │ │ ├─ Applying: ConstructorEnhancer (priority: 95)",
- "[DEBUG] │ │ ├─ Applying: WithInterfaceEnhancer (priority: 95)",
- "[DEBUG] │ │ ├─ Applying: InterfaceEnhancer (priority: 90)",
- "[DEBUG] │ │ ├─ Applying: ConditionalEnhancer (priority: 80)",
- "[DEBUG] │ │ ├─ Added conditional methods to builder VerboseTestBuilder",
- "[DEBUG] │ │ ├─ Applying: ClassJavaDocEnhancer (priority: 10)",
- "[DEBUG] │ │ └─ Applied 8 builder enhancers",
- "[DEBUG] │ ├─ Finalizing builder definition",
- "[DEBUG] │ │ ├─ Resolving method conflicts",
- "[DEBUG] │ │ │ └─ Resolved: 10 signatures, 0 conflicts, 0 methods removed",
- "[DEBUG] │ │ └─ Finalized: 1 class fields, 5 builder-level methods, 2 constructors",
- "[DEBUG] │ ├─ Builder will be generated as: VerboseTestBuilder",
- "[DEBUG] │ └─ Builder definition extracted: VerboseTestBuilder",
+ "[DEBUG] simple-builders: Processing element: VerboseTest",
+ "[DEBUG] ├─ simple-builders: Extracting builder definition from: test.VerboseTest",
+ "[DEBUG] │ ├─ simple-builders: Builder will be generated as: test.VerboseTestBuilder",
+ "[DEBUG] │ ├─ simple-builders: Analysing setters for finding fields",
+ "[DEBUG] │ │ ├─ simple-builders: Analyzing method: setName(java.lang.String)",
+ "[DEBUG] │ │ │ ├─ simple-builders: Processing method generators",
+ "[DEBUG] │ │ │ │ ├─ simple-builders: Applying: BasicSetterGenerator (priority: 100)",
+ "[DEBUG] │ │ │ │ ├─ simple-builders: Applying: StringFormatHelperGenerator (priority: 80)",
+ "[DEBUG] │ │ │ │ ├─ simple-builders: Applying: SupplierMethodGenerator (priority: 60)",
+ "[DEBUG] │ │ │ │ ├─ simple-builders: Applying: StringBuilderConsumerGenerator (priority: 45)",
+ "[DEBUG] │ │ │ │ ├─ simple-builders: Applying: UpdateHelperGenerator (priority: 31)",
+ "[DEBUG] │ │ │ │ └─ simple-builders: Generated 5 methods",
+ "[DEBUG] │ │ │ └─ simple-builders: Adding field: name (type: java.lang.String)",
+ "[DEBUG] │ │ └─ simple-builders: Processed 1 possible setters: added 1 fields, skipped 0",
+ "[DEBUG] │ ├─ simple-builders: Processing class based enhancer",
+ "[DEBUG] │ │ ├─ simple-builders: Applying: GeneratedAnnotationEnhancer (priority: 120)",
+ "[DEBUG] │ │ ├─ simple-builders: Applying: BuilderImplementationAnnotationEnhancer (priority: 115)",
+ "[DEBUG] │ │ ├─ simple-builders: Added @BuilderImplementation annotation to builder VerboseTestBuilder",
+ "[DEBUG] │ │ ├─ simple-builders: Applying: CoreMethodsEnhancer (priority: 100)",
+ "[DEBUG] │ │ ├─ simple-builders: Applying: ConstructorEnhancer (priority: 95)",
+ "[DEBUG] │ │ ├─ simple-builders: Applying: WithInterfaceEnhancer (priority: 95)",
+ "[DEBUG] │ │ ├─ simple-builders: Applying: InterfaceEnhancer (priority: 90)",
+ "[DEBUG] │ │ ├─ simple-builders: Applying: ConditionalEnhancer (priority: 80)",
+ "[DEBUG] │ │ ├─ simple-builders: Added conditional methods to builder VerboseTestBuilder",
+ "[DEBUG] │ │ ├─ simple-builders: Applying: ClassJavaDocEnhancer (priority: 10)",
+ "[DEBUG] │ │ └─ simple-builders: Applied 8 builder enhancers",
+ "[DEBUG] │ ├─ simple-builders: Finalizing builder definition",
+ "[DEBUG] │ │ ├─ simple-builders: Resolving method conflicts",
+ "[DEBUG] │ │ │ └─ simple-builders: Resolved: 10 signatures, 0 conflicts, 0 methods removed",
+ "[DEBUG] │ │ └─ simple-builders: Finalized: 1 class fields, 5 builder-level methods, 2 constructors",
+ "[DEBUG] │ ├─ simple-builders: Builder will be generated as: VerboseTestBuilder",
+ "[DEBUG] │ └─ simple-builders: Builder definition extracted: VerboseTestBuilder",
// Round 1 — code generation
- "[DEBUG] Generating targets for: VerboseTest",
- "[DEBUG] ├─ Code generation for class: VerboseTestBuilder",
- "[DEBUG] │ ├─ JavaClassSource created",
- "[DEBUG] │ ├─ Class metadata added",
- "[DEBUG] │ ├─ Generating 1 fields",
- "[DEBUG] │ │ └─ Fields added: 1 fields",
- "[DEBUG] │ ├─ Generating 2 constructors",
- "[DEBUG] │ │ └─ Constructors added: 2",
- "[DEBUG] │ ├─ Generating 10 method candidates",
- "[DEBUG] │ │ ├─ Resolved to 10 methods after conflict resolution",
- "[DEBUG] │ │ └─ Methods added: 10",
- "[DEBUG] │ ├─ Generating 1 nested type(s)",
- "[DEBUG] │ │ ├─ Generated nested type: With",
- "[DEBUG] │ │ └─ Nested types added",
- "[DEBUG] │ ├─ Class-level annotations added",
- "[DEBUG] │ ├─ Using JDT source formatting with Eclipse formatter"
+ "[DEBUG] simple-builders: Generating targets for: VerboseTest",
+ "[DEBUG] ├─ simple-builders: Code generation for class: VerboseTestBuilder",
+ "[DEBUG] │ ├─ simple-builders: JavaClassSource created",
+ "[DEBUG] │ ├─ simple-builders: Class metadata added",
+ "[DEBUG] │ ├─ simple-builders: Generating 1 fields",
+ "[DEBUG] │ │ └─ simple-builders: Fields added: 1 fields",
+ "[DEBUG] │ ├─ simple-builders: Generating 2 constructors",
+ "[DEBUG] │ │ └─ simple-builders: Constructors added: 2",
+ "[DEBUG] │ ├─ simple-builders: Generating 10 method candidates",
+ "[DEBUG] │ │ ├─ simple-builders: Resolved to 10 methods after conflict resolution",
+ "[DEBUG] │ │ └─ simple-builders: Methods added: 10",
+ "[DEBUG] │ ├─ simple-builders: Generating 1 nested type(s)",
+ "[DEBUG] │ │ ├─ simple-builders: Generated nested type: With",
+ "[DEBUG] │ │ └─ simple-builders: Nested types added",
+ "[DEBUG] │ ├─ simple-builders: Class-level annotations added",
+ "[DEBUG] │ ├─ simple-builders: Using JDT source formatting with Eclipse formatter"
+ " profile 'eclipse-java-format.xml'.",
- "[DEBUG] │ ├─ Writing class to file: test.VerboseTestBuilder",
- "[DEBUG] │ └─ Successfully generated class: VerboseTestBuilder",
- "[DEBUG] ├─ Jackson module entry added",
- "[DEBUG] └─ Generated builder with 1 fields and 10 methods for VerboseTestBuilder",
+ "[DEBUG] │ ├─ simple-builders: Writing class to file: test.VerboseTestBuilder",
+ "[DEBUG] │ └─ simple-builders: Successfully generated class: VerboseTestBuilder",
+ "[DEBUG] ├─ simple-builders: Jackson module entry added",
+ "[DEBUG] └─ simple-builders: Generated builder with 1 fields and 10 methods for VerboseTestBuilder",
"simple-builders: Successfully generated 1 target(s) in this processing round",
// Round 2 — no new elements
"simple-builders: PROCESSING ROUND START",
@@ -1502,7 +1502,7 @@ public class ForcedOldRelease { public ForcedOldRelease() {} }
assertThat(compilation).failed();
assertThat(compilation)
.hadErrorContaining(
- "simple-builders requires Java 17 or higher for annotation processing.");
+ "simple-builders: requires Java 17 or higher for annotation processing.");
}
@Test
From 3725f42edbd9b41b94984a26d01e74bf0ff1a451 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 23:40:19 +0000
Subject: [PATCH 083/170] =?UTF-8?q?logging:=20tag=20renamed=20to=20logging?=
=?UTF-8?q?Prefix=20=E2=80=94=20names=20what=20it=20is?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/logging/ProcessingLogger.java | 38 ++++++++++---------
1 file changed, 20 insertions(+), 18 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
index 4da738a2..9ac6b27d 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
@@ -32,8 +32,8 @@
/**
* Logger for all messages during annotation processing. Providing util-functions for posting
* messages easily. Using a {@code javax.annotation.processing.Messager} inside to publish the
- * messages. Every emitted message is prefixed with the logger's diagnostic tag ("{@code tag: }")
- * when one was supplied at creation.
+ * messages. Every emitted message is prefixed with the logger's diagnostic prefix ("{@code prefix:
+ * }") when one was supplied at creation.
*/
public class ProcessingLogger {
/** Util class for exposing messages of type {@code javax.annotation.processing.Messager}. */
@@ -42,8 +42,8 @@ public class ProcessingLogger {
/** Verbosity level of this logger. */
private final Verbosity verbosity;
- /** Diagnostic tag identifying the processor — every emitted message is prefixed with it. */
- private final String tag;
+ /** Diagnostic prefix identifying the processor — every emitted message is prefixed with it. */
+ private final String loggingPrefix;
/** Thread-local indentation level for hierarchical logging. */
private static final ThreadLocal indentationLevel = ThreadLocal.withInitial(() -> 0);
@@ -69,14 +69,14 @@ public static ProcessingLogger create(ProcessingEnvironment processingEnv) {
}
/**
- * Creates a new ProcessingLogger carrying a diagnostic tag.
+ * Creates a new ProcessingLogger carrying a diagnostic prefix.
*
* @param processingEnv the processing environment providing the messager
- * @param tag the processor's diagnostic tag, or {@code null}
+ * @param loggingPrefix the processor's diagnostic prefix, or {@code null}
* @return a processing logger reporting errors, warnings and info messages
*/
- public static ProcessingLogger create(ProcessingEnvironment processingEnv, String tag) {
- return new ProcessingLogger(processingEnv, Verbosity.NORMAL, tag);
+ public static ProcessingLogger create(ProcessingEnvironment processingEnv, String loggingPrefix) {
+ return new ProcessingLogger(processingEnv, Verbosity.NORMAL, loggingPrefix);
}
/**
@@ -90,34 +90,36 @@ public static ProcessingLogger createVerbose(ProcessingEnvironment processingEnv
}
/**
- * Creates a new debug-enabled ProcessingLogger carrying a diagnostic tag.
+ * Creates a new debug-enabled ProcessingLogger carrying a diagnostic prefix.
*
* @param processingEnv the processing environment providing the messager
- * @param tag the processor's diagnostic tag, or {@code null}
+ * @param loggingPrefix the processor's diagnostic prefix, or {@code null}
* @return a debug-enabled processing logger
*/
- public static ProcessingLogger createVerbose(ProcessingEnvironment processingEnv, String tag) {
- return new ProcessingLogger(processingEnv, Verbosity.VERBOSE, tag);
+ public static ProcessingLogger createVerbose(
+ ProcessingEnvironment processingEnv, String loggingPrefix) {
+ return new ProcessingLogger(processingEnv, Verbosity.VERBOSE, loggingPrefix);
}
- private ProcessingLogger(ProcessingEnvironment processingEnv, Verbosity verbosity, String tag) {
+ private ProcessingLogger(
+ ProcessingEnvironment processingEnv, Verbosity verbosity, String loggingPrefix) {
// Reset ThreadLocal state to ensure clean state between test runs
resetThreadLocalState();
this.messager = processingEnv.getMessager();
this.verbosity = verbosity;
- this.tag = tag;
+ this.loggingPrefix = loggingPrefix;
}
/**
- * Applies this logger's "{@code tag: }" prefix to a message — the one place the tag-prefix
- * pattern is defined. Untagged loggers return the message unchanged.
+ * Applies this logger's "{@code prefix: }" prefix to a message — the one place the prefix pattern
+ * is defined. Loggers without a prefix return the message unchanged.
*
* @param message the message or message format
- * @return the message prefixed with this logger's tag
+ * @return the message prefixed with this logger's prefix
*/
private String prefixed(String message) {
- return tag != null ? tag + ": " + message : message;
+ return loggingPrefix != null ? loggingPrefix + ": " + message : message;
}
/** Resets ThreadLocal state to ensure clean state between test runs. */
From 42c85ded661baf4dec634926a11a2b786ce68c4b Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 23:41:24 +0000
Subject: [PATCH 084/170] logging: prefix sits between the level marker and
tree markers
---
docs/CONTRIBUTING.md | 20 ++--
docs/DEBUG_LOGGING.md | 56 ++++-----
.../processing/logging/ProcessingLogger.java | 27 +++--
.../processor/BuilderProcessorTest.java | 110 +++++++++---------
4 files changed, 106 insertions(+), 107 deletions(-)
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 6c5448e0..ceec7365 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -244,17 +244,17 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md).
[DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor.
[DEBUG] simple-builders: 1 of 1 annotated element(s) are inside the builderGenerationPackages scope.
[DEBUG] simple-builders: Processing element: Project
-[DEBUG] ├─ simple-builders: Extracting builder definition from: test.Project
-[DEBUG] │ ├─ simple-builders: Builder will be generated as: test.ProjectBuilder
-[DEBUG] │ ├─ simple-builders: Analysing setters for finding fields
-[DEBUG] │ │ ├─ simple-builders: Analyzing method: setName(java.lang.String)
-[DEBUG] │ │ │ └─ simple-builders: Adding field: name (type: java.lang.String)
-[DEBUG] │ └─ simple-builders: Processed 1 possible setters: added 1 fields, skipped 0
+[DEBUG] simple-builders: ├─ Extracting builder definition from: test.Project
+[DEBUG] simple-builders: │ ├─ Builder will be generated as: test.ProjectBuilder
+[DEBUG] simple-builders: │ ├─ Analysing setters for finding fields
+[DEBUG] simple-builders: │ │ ├─ Analyzing method: setName(java.lang.String)
+[DEBUG] simple-builders: │ │ │ └─ Adding field: name (type: java.lang.String)
+[DEBUG] simple-builders: │ └─ Processed 1 possible setters: added 1 fields, skipped 0
[DEBUG] simple-builders: Generating targets for: Project
-[DEBUG] ├─ simple-builders: Code generation for builder: ProjectBuilder
-[DEBUG] │ ├─ simple-builders: Adding Methods for 4 candidates
-[DEBUG] │ │ └─ simple-builders: 4 Methods added
-[DEBUG] │ └─ simple-builders: Successfully generated builder: ProjectBuilder
+[DEBUG] simple-builders: ├─ Code generation for builder: ProjectBuilder
+[DEBUG] simple-builders: │ ├─ Adding Methods for 4 candidates
+[DEBUG] simple-builders: │ │ └─ 4 Methods added
+[DEBUG] simple-builders: │ └─ Successfully generated builder: ProjectBuilder
simple-builders: Successfully generated 1 target(s) in this processing round
=============================================
diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md
index 97360650..9ad25592 100644
--- a/docs/DEBUG_LOGGING.md
+++ b/docs/DEBUG_LOGGING.md
@@ -80,7 +80,7 @@ The Simple Builders processor provides **conditional hierarchical logging** to b
- Provides full visibility into the processing hierarchy
- Example: `[INFO] │ └─ simple-builders: Successfully generated 3 target(s) in this processing round`
- Example: `[WARNING] │ │ ├─ Builder field conflict: field 'name'...`
-- Example: `[INFO] [DEBUG] │ │ │ └─ simple-builders: Processing method: setName`
+- Example: `[INFO] [DEBUG] simple-builders: │ │ │ └─ Processing method: setName`
- All message types align vertically despite different prefix lengths
This approach ensures clean production logs while maintaining full debugging capabilities when needed.
@@ -96,39 +96,39 @@ When debug logging is enabled, you'll see detailed output with visual separators
[INFO] [DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor.
[INFO] [DEBUG] simple-builders: 3 of 3 annotated element(s) are inside the builderGenerationPackages scope.
[INFO] [DEBUG] simple-builders: Processing element: PersonDto
-[INFO] [DEBUG] ├─ simple-builders: Extracting builder definition from: org.example.PersonDto
-[INFO] [DEBUG] │ ├─ simple-builders: Builder will be generated as: org.example.PersonDtoBuilder
-[INFO] [DEBUG] │ ├─ simple-builders: Analysing setters for finding fields
-[INFO] [DEBUG] │ │ ├─ simple-builders: Analyzing method: setName(java.lang.String)
-[INFO] [DEBUG] │ │ │ └─ simple-builders: Adding field: name (type: java.lang.String)
-[INFO] [DEBUG] │ │ ├─ simple-builders: Analyzing method: setAge(int)
-[INFO] [DEBUG] │ │ │ └─ simple-builders: Adding field: age (type: int)
-[INFO] [DEBUG] │ └─ simple-builders: Processed 2 possible setters: added 2 fields, skipped 0
+[INFO] [DEBUG] simple-builders: ├─ Extracting builder definition from: org.example.PersonDto
+[INFO] [DEBUG] simple-builders: │ ├─ Builder will be generated as: org.example.PersonDtoBuilder
+[INFO] [DEBUG] simple-builders: │ ├─ Analysing setters for finding fields
+[INFO] [DEBUG] simple-builders: │ │ ├─ Analyzing method: setName(java.lang.String)
+[INFO] [DEBUG] simple-builders: │ │ │ └─ Adding field: name (type: java.lang.String)
+[INFO] [DEBUG] simple-builders: │ │ ├─ Analyzing method: setAge(int)
+[INFO] [DEBUG] simple-builders: │ │ │ └─ Adding field: age (type: int)
+[INFO] [DEBUG] simple-builders: │ └─ Processed 2 possible setters: added 2 fields, skipped 0
[INFO] [DEBUG] simple-builders: Processing element: OrderDto
-[INFO] [DEBUG] ├─ simple-builders: Extracting builder definition from: org.example.OrderDto
-[INFO] [DEBUG] │ └─ simple-builders: Builder will be generated as: org.example.OrderDtoBuilder
-[INFO] [DEBUG] │ └─ simple-builders: Processed 1 possible setters: added 1 fields, skipped 0
+[INFO] [DEBUG] simple-builders: ├─ Extracting builder definition from: org.example.OrderDto
+[INFO] [DEBUG] simple-builders: │ └─ Builder will be generated as: org.example.OrderDtoBuilder
+[INFO] [DEBUG] simple-builders: │ └─ Processed 1 possible setters: added 1 fields, skipped 0
[INFO] [DEBUG] simple-builders: Processing element: CustomerDto (with conflicts)
-[INFO] [DEBUG] ├─ simple-builders: Extracting builder definition from: org.example.CustomerDto
-[INFO] [DEBUG] │ └─ simple-builders: Builder will be generated as: org.example.CustomerDtoBuilder
-[INFO] [DEBUG] │ └─ simple-builders: Processed 2 possible setters: added 2 fields, skipped 0
+[INFO] [DEBUG] simple-builders: ├─ Extracting builder definition from: org.example.CustomerDto
+[INFO] [DEBUG] simple-builders: │ └─ Builder will be generated as: org.example.CustomerDtoBuilder
+[INFO] [DEBUG] simple-builders: │ └─ Processed 2 possible setters: added 2 fields, skipped 0
[WARNING] │ │ └─ simple-builders: Builder field conflict: field 'name' (type Optional) renamed to 'nameOptional' to avoid conflict
[INFO] [DEBUG] simple-builders: Generating targets for: PersonDto
-[INFO] [DEBUG] ├─ simple-builders: Code generation for builder: PersonDtoBuilder
-[INFO] [DEBUG] │ ├─ simple-builders: Class builder created
-[INFO] [DEBUG] │ ├─ simple-builders: Generating 0 constructor fields and 2 setter fields
-[INFO] [DEBUG] │ │ └─ simple-builders: Fields added: 2 fields
-[INFO] [DEBUG] │ ├─ simple-builders: Adding Methods for 4 candidates
-[INFO] [DEBUG] │ │ └─ simple-builders: 4 Methods added
-[INFO] [DEBUG] │ ├─ simple-builders: Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'.
-[INFO] [DEBUG] │ ├─ simple-builders: Writing builder class to file: org.example.PersonDtoBuilder
-[INFO] [DEBUG] │ └─ simple-builders: Successfully generated builder: PersonDtoBuilder
+[INFO] [DEBUG] simple-builders: ├─ Code generation for builder: PersonDtoBuilder
+[INFO] [DEBUG] simple-builders: │ ├─ Class builder created
+[INFO] [DEBUG] simple-builders: │ ├─ Generating 0 constructor fields and 2 setter fields
+[INFO] [DEBUG] simple-builders: │ │ └─ Fields added: 2 fields
+[INFO] [DEBUG] simple-builders: │ ├─ Adding Methods for 4 candidates
+[INFO] [DEBUG] simple-builders: │ │ └─ 4 Methods added
+[INFO] [DEBUG] simple-builders: │ ├─ Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'.
+[INFO] [DEBUG] simple-builders: │ ├─ Writing builder class to file: org.example.PersonDtoBuilder
+[INFO] [DEBUG] simple-builders: │ └─ Successfully generated builder: PersonDtoBuilder
[INFO] [DEBUG] simple-builders: Generating targets for: OrderDto
-[INFO] [DEBUG] ├─ simple-builders: Code generation for builder: OrderDtoBuilder
-[INFO] [DEBUG] │ └─ simple-builders: Successfully generated builder: OrderDtoBuilder
+[INFO] [DEBUG] simple-builders: ├─ Code generation for builder: OrderDtoBuilder
+[INFO] [DEBUG] simple-builders: │ └─ Successfully generated builder: OrderDtoBuilder
[INFO] [DEBUG] simple-builders: Generating targets for: CustomerDto
-[INFO] [DEBUG] ├─ simple-builders: Code generation for builder: CustomerDtoBuilder
-[INFO] [DEBUG] │ └─ simple-builders: Successfully generated builder: CustomerDtoBuilder
+[INFO] [DEBUG] simple-builders: ├─ Code generation for builder: CustomerDtoBuilder
+[INFO] [DEBUG] simple-builders: │ └─ Successfully generated builder: CustomerDtoBuilder
[INFO] simple-builders: Successfully generated 3 target(s) in this processing round
[INFO] simple-builders: PROCESSING ROUND START
[INFO] [DEBUG] simple-builders: Processing round started.
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
index 9ac6b27d..b3de8e9a 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java
@@ -169,7 +169,7 @@ public void info(String message) {
private String formatHierarchicalMessage(String message, int spaces) {
String prefix = getCurrentIndentationLevel() > 0 ? "└─ " : "";
String spacing = " ".repeat(spaces);
- return String.format("%s%s", spacing, formatWithIndentationNoDebug(message, prefix));
+ return spacing + prefixed(formatWithIndentationNoDebug(message, prefix));
}
/**
@@ -179,7 +179,7 @@ private String formatHierarchicalMessage(String message, int spaces) {
* @param args arguments referenced by the format specifiers in the format string
*/
public void info(String format, Object... args) {
- String message = String.format(prefixed(format), args);
+ String message = String.format(format, args);
if (verbosity == Verbosity.VERBOSE) {
// When debug is enabled, add spaces to align with [DEBUG] prefix (which is 6 characters
// longer)
@@ -187,7 +187,7 @@ public void info(String format, Object... args) {
messager.printMessage(Diagnostic.Kind.NOTE, indentedMessage);
} else {
// When debug is disabled, use flat formatting (current behavior)
- messager.printMessage(Diagnostic.Kind.NOTE, message);
+ messager.printMessage(Diagnostic.Kind.NOTE, prefixed(message));
}
}
@@ -200,7 +200,7 @@ public void info(String format, Object... args) {
*/
public void debug(String message) {
if (verbosity == Verbosity.VERBOSE) {
- String indentedMessage = formatWithIndentation(prefixed(message));
+ String indentedMessage = formatWithIndentation(message);
messager.printMessage(Diagnostic.Kind.OTHER, indentedMessage);
}
}
@@ -214,7 +214,7 @@ public void debug(String message) {
*/
public void debug(String format, Object... args) {
if (verbosity == Verbosity.VERBOSE) {
- String message = String.format(prefixed(format), args);
+ String message = String.format(format, args);
String indentedMessage = formatWithIndentation(message);
messager.printMessage(Diagnostic.Kind.OTHER, indentedMessage);
}
@@ -228,7 +228,7 @@ public void debug(String format, Object... args) {
* @param args arguments referenced by the format specifiers in the format string
*/
public void warning(String format, Object... args) {
- String message = String.format(prefixed(format), args);
+ String message = String.format(format, args);
if (verbosity == Verbosity.VERBOSE) {
// When debug is enabled, add spaces to align with [DEBUG] prefix (which is 5 characters
// longer)
@@ -236,7 +236,7 @@ public void warning(String format, Object... args) {
messager.printMessage(Diagnostic.Kind.WARNING, indentedMessage);
} else {
// When debug is disabled, print flat
- messager.printMessage(Diagnostic.Kind.WARNING, message);
+ messager.printMessage(Diagnostic.Kind.WARNING, prefixed(message));
}
}
@@ -248,7 +248,7 @@ public void warning(String format, Object... args) {
* @param args arguments referenced by the format specifiers in the format string
*/
public void warning(Element e, String format, Object... args) {
- String message = String.format(prefixed(format), args);
+ String message = String.format(format, args);
if (verbosity == Verbosity.VERBOSE) {
// When debug is enabled, add spaces to align with [DEBUG] prefix (which is 5 characters
// longer)
@@ -256,7 +256,7 @@ public void warning(Element e, String format, Object... args) {
messager.printMessage(Diagnostic.Kind.WARNING, indentedMessage, e);
} else {
// When debug is disabled, print flat
- messager.printMessage(Diagnostic.Kind.WARNING, message, e);
+ messager.printMessage(Diagnostic.Kind.WARNING, prefixed(message), e);
}
}
@@ -271,7 +271,7 @@ private String formatWithIndentation(String message, String prefix) {
int level = indentationLevel.get();
if (level == 0) {
// At level 0, no indentation or prefix
- return "[DEBUG] " + message;
+ return "[DEBUG] " + prefixed(message);
}
// Use │ characters with proper spacing for better visual connection between hierarchical levels
@@ -281,7 +281,7 @@ private String formatWithIndentation(String message, String prefix) {
}
// Add the specified prefix
indent.append(prefix);
- return "[DEBUG] " + indent + message;
+ return "[DEBUG] " + prefixed(indent.toString() + message);
}
/**
@@ -330,8 +330,7 @@ public void debugStartOperation(String formatClosingMessage, Object... args) {
// Log the operation start message with proper prefix handling
if (verbosity == Verbosity.VERBOSE) {
- String operationMessage =
- formatWithIndentation(String.format(prefixed(formatClosingMessage), args));
+ String operationMessage = formatWithIndentation(String.format(formatClosingMessage, args));
messager.printMessage(Diagnostic.Kind.NOTE, operationMessage);
}
@@ -364,7 +363,7 @@ public void debugEndOperation(String formatClosingMessage, Object... args) {
// Log the closing message with └─ to indicate it's the last operation at this level
if (verbosity == Verbosity.VERBOSE) {
- String closingMessage = String.format(prefixed(formatClosingMessage), args);
+ String closingMessage = String.format(formatClosingMessage, args);
String operationMessage = formatWithIndentation(closingMessage, "└─ ");
messager.printMessage(Diagnostic.Kind.NOTE, operationMessage);
}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
index 53ca6b59..8bda3f72 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java
@@ -94,8 +94,8 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() {
"[DEBUG] simple-builders: Starting BuilderProcessor...",
"[DEBUG] simple-builders: Loaded global configuration from compiler arguments: BuilderConfiguration[verbose=ENABLED]",
"[DEBUG] simple-builders: Initializing generator registry",
- "[DEBUG] ├─ simple-builders: Loaded 15 method generators and 10 builder enhancers total",
- "[DEBUG] └─ simple-builders: Initialized GeneratorRegistry with 15 method generators and 10 builder",
+ "[DEBUG] simple-builders: ├─ Loaded 15 method generators and 10 builder enhancers total",
+ "[DEBUG] simple-builders: └─ Initialized GeneratorRegistry with 15 method generators and 10 builder",
// Round 1 — start
"simple-builders: PROCESSING ROUND START",
"[DEBUG] simple-builders: Processing round started.",
@@ -103,66 +103,66 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() {
"[DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor.",
// Round 1 — configuration resolution
"[DEBUG] simple-builders: Processing element: VerboseTest",
- "[DEBUG] ├─ simple-builders: Resolving configuration for element: VerboseTest",
- "[DEBUG] │ ├─ simple-builders: Built-in template @SimpleBuilder found in DIRECT scope",
- "[DEBUG] │ └─ simple-builders: Resulting configuration resolved: BuilderConfiguration[",
+ "[DEBUG] simple-builders: ├─ Resolving configuration for element: VerboseTest",
+ "[DEBUG] simple-builders: │ ├─ Built-in template @SimpleBuilder found in DIRECT scope",
+ "[DEBUG] simple-builders: │ └─ Resulting configuration resolved: BuilderConfiguration[",
"[DEBUG] simple-builders: 1 of 1 annotated element(s) are inside the"
+ " builderGenerationPackages scope.",
// Round 1 — builder definition extraction
"[DEBUG] simple-builders: Processing element: VerboseTest",
- "[DEBUG] ├─ simple-builders: Extracting builder definition from: test.VerboseTest",
- "[DEBUG] │ ├─ simple-builders: Builder will be generated as: test.VerboseTestBuilder",
- "[DEBUG] │ ├─ simple-builders: Analysing setters for finding fields",
- "[DEBUG] │ │ ├─ simple-builders: Analyzing method: setName(java.lang.String)",
- "[DEBUG] │ │ │ ├─ simple-builders: Processing method generators",
- "[DEBUG] │ │ │ │ ├─ simple-builders: Applying: BasicSetterGenerator (priority: 100)",
- "[DEBUG] │ │ │ │ ├─ simple-builders: Applying: StringFormatHelperGenerator (priority: 80)",
- "[DEBUG] │ │ │ │ ├─ simple-builders: Applying: SupplierMethodGenerator (priority: 60)",
- "[DEBUG] │ │ │ │ ├─ simple-builders: Applying: StringBuilderConsumerGenerator (priority: 45)",
- "[DEBUG] │ │ │ │ ├─ simple-builders: Applying: UpdateHelperGenerator (priority: 31)",
- "[DEBUG] │ │ │ │ └─ simple-builders: Generated 5 methods",
- "[DEBUG] │ │ │ └─ simple-builders: Adding field: name (type: java.lang.String)",
- "[DEBUG] │ │ └─ simple-builders: Processed 1 possible setters: added 1 fields, skipped 0",
- "[DEBUG] │ ├─ simple-builders: Processing class based enhancer",
- "[DEBUG] │ │ ├─ simple-builders: Applying: GeneratedAnnotationEnhancer (priority: 120)",
- "[DEBUG] │ │ ├─ simple-builders: Applying: BuilderImplementationAnnotationEnhancer (priority: 115)",
- "[DEBUG] │ │ ├─ simple-builders: Added @BuilderImplementation annotation to builder VerboseTestBuilder",
- "[DEBUG] │ │ ├─ simple-builders: Applying: CoreMethodsEnhancer (priority: 100)",
- "[DEBUG] │ │ ├─ simple-builders: Applying: ConstructorEnhancer (priority: 95)",
- "[DEBUG] │ │ ├─ simple-builders: Applying: WithInterfaceEnhancer (priority: 95)",
- "[DEBUG] │ │ ├─ simple-builders: Applying: InterfaceEnhancer (priority: 90)",
- "[DEBUG] │ │ ├─ simple-builders: Applying: ConditionalEnhancer (priority: 80)",
- "[DEBUG] │ │ ├─ simple-builders: Added conditional methods to builder VerboseTestBuilder",
- "[DEBUG] │ │ ├─ simple-builders: Applying: ClassJavaDocEnhancer (priority: 10)",
- "[DEBUG] │ │ └─ simple-builders: Applied 8 builder enhancers",
- "[DEBUG] │ ├─ simple-builders: Finalizing builder definition",
- "[DEBUG] │ │ ├─ simple-builders: Resolving method conflicts",
- "[DEBUG] │ │ │ └─ simple-builders: Resolved: 10 signatures, 0 conflicts, 0 methods removed",
- "[DEBUG] │ │ └─ simple-builders: Finalized: 1 class fields, 5 builder-level methods, 2 constructors",
- "[DEBUG] │ ├─ simple-builders: Builder will be generated as: VerboseTestBuilder",
- "[DEBUG] │ └─ simple-builders: Builder definition extracted: VerboseTestBuilder",
+ "[DEBUG] simple-builders: ├─ Extracting builder definition from: test.VerboseTest",
+ "[DEBUG] simple-builders: │ ├─ Builder will be generated as: test.VerboseTestBuilder",
+ "[DEBUG] simple-builders: │ ├─ Analysing setters for finding fields",
+ "[DEBUG] simple-builders: │ │ ├─ Analyzing method: setName(java.lang.String)",
+ "[DEBUG] simple-builders: │ │ │ ├─ Processing method generators",
+ "[DEBUG] simple-builders: │ │ │ │ ├─ Applying: BasicSetterGenerator (priority: 100)",
+ "[DEBUG] simple-builders: │ │ │ │ ├─ Applying: StringFormatHelperGenerator (priority: 80)",
+ "[DEBUG] simple-builders: │ │ │ │ ├─ Applying: SupplierMethodGenerator (priority: 60)",
+ "[DEBUG] simple-builders: │ │ │ │ ├─ Applying: StringBuilderConsumerGenerator (priority: 45)",
+ "[DEBUG] simple-builders: │ │ │ │ ├─ Applying: UpdateHelperGenerator (priority: 31)",
+ "[DEBUG] simple-builders: │ │ │ │ └─ Generated 5 methods",
+ "[DEBUG] simple-builders: │ │ │ └─ Adding field: name (type: java.lang.String)",
+ "[DEBUG] simple-builders: │ │ └─ Processed 1 possible setters: added 1 fields, skipped 0",
+ "[DEBUG] simple-builders: │ ├─ Processing class based enhancer",
+ "[DEBUG] simple-builders: │ │ ├─ Applying: GeneratedAnnotationEnhancer (priority: 120)",
+ "[DEBUG] simple-builders: │ │ ├─ Applying: BuilderImplementationAnnotationEnhancer (priority: 115)",
+ "[DEBUG] simple-builders: │ │ ├─ Added @BuilderImplementation annotation to builder VerboseTestBuilder",
+ "[DEBUG] simple-builders: │ │ ├─ Applying: CoreMethodsEnhancer (priority: 100)",
+ "[DEBUG] simple-builders: │ │ ├─ Applying: ConstructorEnhancer (priority: 95)",
+ "[DEBUG] simple-builders: │ │ ├─ Applying: WithInterfaceEnhancer (priority: 95)",
+ "[DEBUG] simple-builders: │ │ ├─ Applying: InterfaceEnhancer (priority: 90)",
+ "[DEBUG] simple-builders: │ │ ├─ Applying: ConditionalEnhancer (priority: 80)",
+ "[DEBUG] simple-builders: │ │ ├─ Added conditional methods to builder VerboseTestBuilder",
+ "[DEBUG] simple-builders: │ │ ├─ Applying: ClassJavaDocEnhancer (priority: 10)",
+ "[DEBUG] simple-builders: │ │ └─ Applied 8 builder enhancers",
+ "[DEBUG] simple-builders: │ ├─ Finalizing builder definition",
+ "[DEBUG] simple-builders: │ │ ├─ Resolving method conflicts",
+ "[DEBUG] simple-builders: │ │ │ └─ Resolved: 10 signatures, 0 conflicts, 0 methods removed",
+ "[DEBUG] simple-builders: │ │ └─ Finalized: 1 class fields, 5 builder-level methods, 2 constructors",
+ "[DEBUG] simple-builders: │ ├─ Builder will be generated as: VerboseTestBuilder",
+ "[DEBUG] simple-builders: │ └─ Builder definition extracted: VerboseTestBuilder",
// Round 1 — code generation
"[DEBUG] simple-builders: Generating targets for: VerboseTest",
- "[DEBUG] ├─ simple-builders: Code generation for class: VerboseTestBuilder",
- "[DEBUG] │ ├─ simple-builders: JavaClassSource created",
- "[DEBUG] │ ├─ simple-builders: Class metadata added",
- "[DEBUG] │ ├─ simple-builders: Generating 1 fields",
- "[DEBUG] │ │ └─ simple-builders: Fields added: 1 fields",
- "[DEBUG] │ ├─ simple-builders: Generating 2 constructors",
- "[DEBUG] │ │ └─ simple-builders: Constructors added: 2",
- "[DEBUG] │ ├─ simple-builders: Generating 10 method candidates",
- "[DEBUG] │ │ ├─ simple-builders: Resolved to 10 methods after conflict resolution",
- "[DEBUG] │ │ └─ simple-builders: Methods added: 10",
- "[DEBUG] │ ├─ simple-builders: Generating 1 nested type(s)",
- "[DEBUG] │ │ ├─ simple-builders: Generated nested type: With",
- "[DEBUG] │ │ └─ simple-builders: Nested types added",
- "[DEBUG] │ ├─ simple-builders: Class-level annotations added",
- "[DEBUG] │ ├─ simple-builders: Using JDT source formatting with Eclipse formatter"
+ "[DEBUG] simple-builders: ├─ Code generation for class: VerboseTestBuilder",
+ "[DEBUG] simple-builders: │ ├─ JavaClassSource created",
+ "[DEBUG] simple-builders: │ ├─ Class metadata added",
+ "[DEBUG] simple-builders: │ ├─ Generating 1 fields",
+ "[DEBUG] simple-builders: │ │ └─ Fields added: 1 fields",
+ "[DEBUG] simple-builders: │ ├─ Generating 2 constructors",
+ "[DEBUG] simple-builders: │ │ └─ Constructors added: 2",
+ "[DEBUG] simple-builders: │ ├─ Generating 10 method candidates",
+ "[DEBUG] simple-builders: │ │ ├─ Resolved to 10 methods after conflict resolution",
+ "[DEBUG] simple-builders: │ │ └─ Methods added: 10",
+ "[DEBUG] simple-builders: │ ├─ Generating 1 nested type(s)",
+ "[DEBUG] simple-builders: │ │ ├─ Generated nested type: With",
+ "[DEBUG] simple-builders: │ │ └─ Nested types added",
+ "[DEBUG] simple-builders: │ ├─ Class-level annotations added",
+ "[DEBUG] simple-builders: │ ├─ Using JDT source formatting with Eclipse formatter"
+ " profile 'eclipse-java-format.xml'.",
- "[DEBUG] │ ├─ simple-builders: Writing class to file: test.VerboseTestBuilder",
- "[DEBUG] │ └─ simple-builders: Successfully generated class: VerboseTestBuilder",
- "[DEBUG] ├─ simple-builders: Jackson module entry added",
- "[DEBUG] └─ simple-builders: Generated builder with 1 fields and 10 methods for VerboseTestBuilder",
+ "[DEBUG] simple-builders: │ ├─ Writing class to file: test.VerboseTestBuilder",
+ "[DEBUG] simple-builders: │ └─ Successfully generated class: VerboseTestBuilder",
+ "[DEBUG] simple-builders: ├─ Jackson module entry added",
+ "[DEBUG] simple-builders: └─ Generated builder with 1 fields and 10 methods for VerboseTestBuilder",
"simple-builders: Successfully generated 1 target(s) in this processing round",
// Round 2 — no new elements
"simple-builders: PROCESSING ROUND START",
From 8cc3655fd32d435b8a0df6088bd5a7e5a0f20ff3 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 23:41:37 +0000
Subject: [PATCH 085/170] =?UTF-8?q?collector:=20drop=20redundant=20direct-?=
=?UTF-8?q?match=20in=20matchingAnnotations=20=E2=80=94=20Class=20overload?=
=?UTF-8?q?=20covers=20it?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../javahelpers/codegen/processing/ElementCollector.java | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
index ad858c3e..5d091a98 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java
@@ -141,13 +141,11 @@ private static List matchingAnnotations(
if (annotation.getKind() != ElementKind.ANNOTATION_TYPE) {
continue;
}
- boolean direct =
- selector.annotationTypes().stream()
- .anyMatch(
- type -> type.getCanonicalName().equals(annotation.getQualifiedName().toString()));
+ // Direct annotation types are already covered by the Class overload above — only
+ // meta-annotated types (e.g. template annotations carrying the marker) remain to match
boolean metaMarked =
selector.metaMarker() != null && annotation.getAnnotation(selector.metaMarker()) != null;
- if (direct || metaMarked) {
+ if (metaMarked) {
result.add(annotation);
}
}
From abb997b97622ecdf0f13ef7299e0c05a75a48962 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 23:42:09 +0000
Subject: [PATCH 086/170] =?UTF-8?q?formatter=20resolver:=20keep=20it=20in?=
=?UTF-8?q?=20the=20processing=20package=20=E2=80=94=20the=20codegen=20mov?=
=?UTF-8?q?e=20is=20the=20next=20PR's?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/AbstractCodegenProcessor.java | 1 +
.../codegen/processing/CodegenProcessorConfiguration.java | 1 +
.../builders/processor}/processing/SourceFormatterResolver.java | 2 +-
3 files changed, 3 insertions(+), 1 deletion(-)
rename processor/src/main/java/org/javahelpers/{codegen => simple/builders/processor}/processing/SourceFormatterResolver.java (98%)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index 0f146c4c..b67f5315 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -42,6 +42,7 @@
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext;
import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader;
+import org.javahelpers.simple.builders.processor.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
index f352a4a4..dd5fe684 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java
@@ -26,6 +26,7 @@
import java.util.List;
import org.javahelpers.simple.builders.processor.classgen.SourceFormatter;
+import org.javahelpers.simple.builders.processor.processing.SourceFormatterResolver;
/**
* Static declarations of an {@link AbstractCodegenProcessor}: everything the framework needs to
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/SourceFormatterResolver.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java
similarity index 98%
rename from processor/src/main/java/org/javahelpers/codegen/processing/SourceFormatterResolver.java
rename to processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java
index d0eab969..a3c117d9 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/SourceFormatterResolver.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java
@@ -22,7 +22,7 @@
* SOFTWARE.
*/
-package org.javahelpers.codegen.processing;
+package org.javahelpers.simple.builders.processor.processing;
import java.util.EnumMap;
import org.javahelpers.simple.builders.core.enums.FormattingMode;
From 1cc7185263f3685c5ca4198c0d257a592dd62ddf Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 23:42:21 +0000
Subject: [PATCH 087/170] =?UTF-8?q?context:=20generatedPackageName=20dropp?=
=?UTF-8?q?ed=20=E2=80=94=20processingTargetPackage=20already=20carries=20?=
=?UTF-8?q?it?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../processing/AnnotationProcessingContext.java | 16 ++--------------
1 file changed, 2 insertions(+), 14 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
index b4446013..aeffe672 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
@@ -82,8 +82,6 @@ public class AnnotationProcessingContext implements TypeMappingContext {
private final GeneratedTypes generatedTypes = new GeneratedTypes();
/** Package generated code is written to, or {@code null} when not generating into a package. */
- private String generatedPackageName;
-
/** Per-target processing state for the type currently being generated. */
private ProcessingTargetPackage> processingTargetPackage;
@@ -120,16 +118,6 @@ public AnnotationProcessingContext(
this.options = options;
}
- /**
- * Sets the name of the package generated code is written to. Called when the current generation
- * target is known.
- *
- * @param generatedPackageName the qualified name of the package generated classes are placed in
- */
- protected void setGeneratedPackageName(String generatedPackageName) {
- this.generatedPackageName = generatedPackageName;
- }
-
/**
* Initializes the per-target state ({@link ProcessingTargetPackage}) for the type currently being
* generated. The processing driver calls this once per planned task before analysis starts, so
@@ -141,7 +129,6 @@ protected void setGeneratedPackageName(String generatedPackageName) {
*/
public void initProcessingTargetPackage(ProcessingTargetPackage> processingTargetPackage) {
this.processingTargetPackage = processingTargetPackage;
- setGeneratedPackageName(processingTargetPackage.targetPackage());
}
/**
@@ -173,7 +160,8 @@ public boolean isAccessibleFromGeneratedPackage(Element member) {
if (member.getModifiers().contains(Modifier.PRIVATE)) {
return false;
}
- return generatedPackageName != null && getPackageName(member).equals(generatedPackageName);
+ return processingTargetPackage != null
+ && getPackageName(member).equals(processingTargetPackage.targetPackage());
}
/**
From c5961c7e0ba09f97949b967daba49e6ac32b36c8 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 23:42:35 +0000
Subject: [PATCH 088/170] scope resolver: read the generated-types registry off
the context at the call sites
---
.../processor/analysis/BuilderScopeResolver.java | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
index bae7f91d..e34749b8 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/BuilderScopeResolver.java
@@ -79,8 +79,6 @@ private record ResolutionInputs(BuilderConfiguration configuration, String build
private ResolutionInputs cachedResolutionInputs;
private PackageScopes usagePackages = PackageScopes.unscoped();
private final Map> resolvedBuilderTypes = new HashMap<>();
- // Generated-types registry lives on the context (shared with the processing driver)
- private final GeneratedTypes generatedBuilders;
/**
* Creates a new resolver for the given processing context.
@@ -89,7 +87,6 @@ private record ResolutionInputs(BuilderConfiguration configuration, String build
*/
public BuilderScopeResolver(BuilderProcessingContext context) {
this.context = context;
- this.generatedBuilders = context.getGeneratedTypes();
}
/**
@@ -203,7 +200,7 @@ public boolean isInGenerationScope(Element element, BuilderConfiguration configu
* @param builderType the generated builder's type name
*/
public void registerGeneratedBuilder(TypeName targetType, TypeName builderType) {
- generatedBuilders.add(targetType, builderType);
+ context.getGeneratedTypes().add(targetType, builderType);
resolvedBuilderTypes.clear();
}
@@ -212,7 +209,7 @@ public void registerGeneratedBuilder(TypeName targetType, TypeName builderType)
* processing round, so stale registrations and resolutions of the previous round are dropped.
*/
public void resetGeneratedBuilders() {
- generatedBuilders.clear();
+ context.getGeneratedTypes().clear();
resolvedBuilderTypes.clear();
}
@@ -234,7 +231,8 @@ private Optional resolve(TypeElement referencedType) {
// Types whose builders are generated in the current processing round are trusted
// immediately — our own generators always produce the builder contract, so no
// classpath lookup or contract check is needed.
- Optional generatedBuilder = generatedBuilders.findGenerated(referencedTypeName);
+ Optional generatedBuilder =
+ context.getGeneratedTypes().findGenerated(referencedTypeName);
if (generatedBuilder.isPresent()) {
// Our generators always emit a static create() and no create(T) - the empty path uses
// the factory, the copy path the constructor
From 295b605375ae5b89f077a196356cfa440df55026 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 23:42:58 +0000
Subject: [PATCH 089/170] =?UTF-8?q?registration:=20planned=20names=20regis?=
=?UTF-8?q?tered=20via=20context.addGeneratedType=20=E2=80=94=20no=20gette?=
=?UTF-8?q?r=20round-trip?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/AbstractCodegenProcessor.java | 10 +++++-----
.../simple/builders/processor/BuilderProcessor.java | 6 ++----
.../processing/AnnotationProcessingContext.java | 11 +++++++++++
3 files changed, 18 insertions(+), 9 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
index b67f5315..1c51dca4 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java
@@ -509,9 +509,9 @@ protected void registerPlannedTaskResults(List> tasks, C context)
continue;
}
TypeName elementName = JavaLangMapper.mapToTypeName(targetType, context);
- for (TypeName generated : plannedTaskResultTypeNames(task, context)) {
- context.getGeneratedTypes().add(elementName, generated);
- onTaskResultRegistered(task, generated, context);
+ for (TypeName planned : plannedTaskResultTypeNames(task, context)) {
+ context.addGeneratedType(elementName, planned);
+ onTaskResultRegistered(task, planned, context);
}
}
}
@@ -533,11 +533,11 @@ protected void registerPlannedTaskResults(List> tasks, C context)
* extras on registration (e.g. SPI publication). Default: nothing.
*
* @param task the planned task
- * @param generatedName the type name just registered for it
+ * @param plannedName the predicted type name just registered for it
* @param context the processing context
*/
// codeql[java/unused-parameter] — extension point; domains may need every parameter.
- protected void onTaskResultRegistered(CodegenTask task, TypeName generatedName, C context) {
+ protected void onTaskResultRegistered(CodegenTask task, TypeName plannedName, C context) {
// Extension point for domains — registration bookkeeping is already done by the driver.
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
index 911b64e6..bf981aa2 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java
@@ -457,13 +457,11 @@ protected List plannedTaskResultTypeNames(
/** SPI-exports the registered builder on top of the generic generated-type registration. */
@Override
protected void onTaskResultRegistered(
- CodegenTask task,
- TypeName generatedName,
- BuilderProcessingContext ctx) {
+ CodegenTask task, TypeName plannedName, BuilderProcessingContext ctx) {
if (task.element() instanceof TypeElement targetType) {
SimpleBuildersSpiIntegration.registerBuilder(
JavaLangMapper.mapToTypeName(targetType, ctx),
- ctx.getBuilderScopeResolver().generatedBuilderContract(generatedName),
+ ctx.getBuilderScopeResolver().generatedBuilderContract(plannedName),
task.configuration().getSetterPrefix());
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
index aeffe672..f17e734b 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java
@@ -240,6 +240,17 @@ public GeneratedTypes getGeneratedTypes() {
return generatedTypes;
}
+ /**
+ * Registers one type planned to be generated for a processed element — a convenience over {@link
+ * #getGeneratedTypes()} for the common registration call.
+ *
+ * @param elementType the processed element's type the generated type belongs to
+ * @param generatedType the type planned to be generated for it
+ */
+ public void addGeneratedType(TypeName elementType, TypeName generatedType) {
+ generatedTypes.add(elementType, generatedType);
+ }
+
/**
* Get the TypeElement for a given qualified class name.
*
From 77779db0ca668acacba1cc1e15166db5232dec47 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 23:43:26 +0000
Subject: [PATCH 090/170] =?UTF-8?q?target-package=20record:=20javadoc=20ba?=
=?UTF-8?q?ck=20in=20the=20original's=20shape=20=E2=80=94=20clearer=20and?=
=?UTF-8?q?=20keeps=20the=20rename=20diff=20pairing?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../codegen/processing/ProcessingTargetPackage.java | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTargetPackage.java b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTargetPackage.java
index 318e9356..f3a3e603 100644
--- a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTargetPackage.java
+++ b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTargetPackage.java
@@ -27,13 +27,13 @@
/**
* Per-target processing state for the type that is currently being generated.
*
- * The processing driver sets one instance per processed type on the processing context before
- * extraction starts, so downstream analysis can reach it without threading both values through
- * every call.
+ *
{@link AbstractCodegenProcessor} sets one instance per processed type on the processing
+ * context before extraction starts, so downstream analysis can reach it without threading both
+ * values through every call.
*
* @param configuration the resolved domain configuration for the current target
- * @param targetPackage the package the generated class is written to. This is the configured output
- * package when set; otherwise a package derived from the processed element itself. It cannot
+ * @param targetPackage the package the generated code is written to. This is the configured output
+ * package option when set; otherwise a package derived from the processed element. It cannot
* always be derived from the processed element itself, which is why it is carried explicitly
* here
* @param the domain configuration type
From 21de6f04d6d9257dbb28c699b15c051a31a94499 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 23:44:11 +0000
Subject: [PATCH 091/170] tests: formatter-resolver import follows the class
back to the processing package
---
.../builders/processor/RoasterCodeGeneratorResilienceTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java
index 5d4b91ef..85f9b3f0 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java
@@ -28,7 +28,6 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import javax.annotation.processing.ProcessingEnvironment;
-import org.javahelpers.codegen.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.core.enums.AccessModifier;
import org.javahelpers.simple.builders.core.enums.FormattingMode;
import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException;
@@ -46,6 +45,7 @@
import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext;
+import org.javahelpers.simple.builders.processor.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
import org.javahelpers.simple.builders.processor.testing.CapturingProcessingLogger;
import org.javahelpers.simple.builders.processor.testing.ProcessingEnvironmentStub;
From 38605949c08101280a643ff34c2201263d6b03a3 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 26 Oct 2026 23:54:05 +0000
Subject: [PATCH 092/170] =?UTF-8?q?tests:=20pin=20the=20complete=20tagged?=
=?UTF-8?q?=20performance-report=20layout=20=E2=80=94=20timings=20normaliz?=
=?UTF-8?q?ed,=20structure=20asserted?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../logging/ActivePerformanceTrackerTest.java | 67 +++++++++++++++++++
.../testing/CapturingProcessingLogger.java | 13 +++-
2 files changed, 79 insertions(+), 1 deletion(-)
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java
index aeb47b9f..270afc86 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java
@@ -31,6 +31,7 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.List;
import org.javahelpers.simple.builders.processor.classgen.GenerationPhases;
import org.javahelpers.simple.builders.processor.processing.PipelinePhases;
import org.javahelpers.simple.builders.processor.testing.CapturingProcessingLogger;
@@ -73,6 +74,72 @@ private JsonNode generateReportAndParseJson(String outputFile) throws IOExceptio
return new ObjectMapper().readTree(jsonContent);
}
+ /**
+ * Pins the complete tagged report layout: every line carries the logger's diagnostic prefix and
+ * the tree connectors sit after it. Timing values are normalized to {@code } — the structure,
+ * labels and indentation are what is asserted.
+ */
+ @Test
+ void generateReport_prefixedLogger_producesExpectedTaggedLayout() {
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(null, PipelinePhases.PIPELINE_HIERARCHY);
+ tracker.startClass("MyClass");
+ tracker.startPhase();
+ tracker.endPhase(PipelinePhases.PHASE_ANALYSIS);
+ tracker.startGenerator();
+ tracker.endGenerator("MyGenerator");
+ tracker.startEnhancer();
+ tracker.endEnhancer("MyEnhancer");
+ tracker.endClass(5, 2);
+
+ CapturingProcessingLogger capturing = CapturingProcessingLogger.create("simple-builders");
+ tracker.generateReport(capturing.logger());
+
+ List actual =
+ capturing.messages().stream()
+ .map(m -> m.replaceFirst("^[A-Z]+: ", ""))
+ .map(m -> m.replaceAll("\\d+(\\.\\d+)?", ""))
+ .toList();
+
+ assertEquals(
+ List.of(
+ "simple-builders: PERFORMANCE REPORT",
+ "simple-builders: ================================",
+ "simple-builders: Total classes processed: ",
+ "simple-builders: Total processing time: s",
+ "simple-builders: ",
+ "simple-builders: Phase breakdown:",
+ "simple-builders: ├─ Element Collection: s (%)",
+ "simple-builders: ├─ Configuration Resolution: s (%)",
+ "simple-builders: ├─ Analysis: s (%)",
+ "simple-builders: ├─ Target Mapping: s (%)",
+ "simple-builders: └─ Code Generation: s (%)",
+ "simple-builders: ├─ Source Construction: s (%)",
+ "simple-builders: │ ├─ Element Building: s (%)",
+ "simple-builders: │ │ ├─ Class Creation: s (%)",
+ "simple-builders: │ │ ├─ Class Metadata: s (%)",
+ "simple-builders: │ │ ├─ Fields: