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 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 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 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 metaMarker, + Class 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 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 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 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 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 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 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 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 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 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 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 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 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 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 options = List.of(); + private CompilerOption verboseOption; + private CompilerOption formatterProfileOption; + + /** The processor's declared compiler options. */ + public Builder options(Collection 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 metaMarker, - Class optOutAnnotation) {} + Class 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 metaMarker; + private Class 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... annotationTypes) { + this.annotationTypes = Set.of(annotationTypes); + return this; + } + + /** Meta-annotation marking annotation types that also select, or {@code null}. */ + public Builder metaMarker(Class metaMarker) { + this.metaMarker = metaMarker; + return this; + } + + /** Annotation that excludes a selected element again, or {@code null}. */ + public Builder optOutAnnotation(Class 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 annotations, RoundEnvironment roundEnv, C context) {
+      Set 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 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 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 metaMarker,
     Class 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 metaMarker;
     private Class 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 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 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 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 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 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 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 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 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 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 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> 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> tasks, C context) {
     // Extension point for domains — planning bookkeeping is already done by the driver.
   }
@@ -562,6 +567,7 @@ protected void onTasksPlanned(List> 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 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 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 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: s (%)", + "simple-builders: │ │ ├─ Constructors: s (%)", + "simple-builders: │ │ ├─ Methods: s (%)", + "simple-builders: │ │ ├─ Nested Types: s (%)", + "simple-builders: │ │ └─ Class Annotations: s (%)", + "simple-builders: │ ├─ String Generation: s (%)", + "simple-builders: │ └─ Formatting: s (%)", + "simple-builders: └─ File Writing: s (%)", + "simple-builders: ", + "simple-builders: Average per class: ms", + "simple-builders: ", + "simple-builders: Top slowest classes:", + "simple-builders: . MyClass - ms ( fields, collections)", + "simple-builders: ", + "simple-builders: Top slowest MethodGenerators:", + "simple-builders: . MyGenerator - s ( calls, ms/call)", + "simple-builders: ", + "simple-builders: Top slowest BuilderEnhancers:", + "simple-builders: . MyEnhancer - s ( calls, ms/call)"), + actual); + } + @Test void generateReport_withNoData_logsBasicReport() { ActivePerformanceTracker tracker = diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/CapturingProcessingLogger.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/CapturingProcessingLogger.java index c524a352..63961d33 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/CapturingProcessingLogger.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/CapturingProcessingLogger.java @@ -74,9 +74,20 @@ private CapturingProcessingLogger(List messages, ProcessingLogger logger * @return a new capturing logger instance */ public static CapturingProcessingLogger create() { + return create(null); + } + + /** + * Creates a new capturing logger carrying a diagnostic logging prefix. + * + * @param loggingPrefix the logger's diagnostic prefix, or {@code null} for unprefixed output + * @return a new capturing logger instance + */ + public static CapturingProcessingLogger create(String loggingPrefix) { List messages = new ArrayList<>(); ProcessingLogger logger = - ProcessingLogger.create(createCapturingEnvironment(messages, Collections.emptyMap())); + ProcessingLogger.create( + createCapturingEnvironment(messages, Collections.emptyMap()), loggingPrefix); return new CapturingProcessingLogger(messages, logger); } From fa6473f001f9ff732fc57b73bb4125c2f885a274 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:03:13 +0000 Subject: [PATCH 093/170] docs: warning examples and prefix wording to the real emitted shape --- docs/DEBUG_LOGGING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md index 9ad25592..6e5264e1 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -76,10 +76,10 @@ The Simple Builders processor provides **conditional hierarchical logging** to b - 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]` +- Every message carries the `simple-builders:` logger prefix; DEBUG messages are posted on the `[INFO]` chanel so they show as `[INFO] [DEBUG]` - 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] simple-builders: │ └─ Successfully generated 3 target(s) in this processing round` +- Example: `[WARNING] simple-builders: │ │ ├─ Builder field conflict: field 'name'...` - Example: `[INFO] [DEBUG] simple-builders: │ │ │ └─ Processing method: setName` - All message types align vertically despite different prefix lengths @@ -112,7 +112,7 @@ When debug logging is enabled, you'll see detailed output with visual separators [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 +[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 From 1acd60a0e204b069124cecd84478af76198e2d57 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:12:17 +0000 Subject: [PATCH 094/170] context: drop the orphaned field comment the generatedPackageName removal left --- .../processor/processing/AnnotationProcessingContext.java | 1 - 1 file changed, 1 deletion(-) 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 f17e734b..329ef6b8 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 @@ -81,7 +81,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. */ /** Per-target processing state for the type currently being generated. */ private ProcessingTargetPackage processingTargetPackage; From 31322f8c49010a0680221f62890c1c6d3a2198cd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:12:39 +0000 Subject: [PATCH 095/170] =?UTF-8?q?tests:=20perf-report=20pin=20as=20a=20t?= =?UTF-8?q?ext=20block=20=E2=80=94=20one=20full=20compare,=20normalization?= =?UTF-8?q?=20commented?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logging/ActivePerformanceTrackerTest.java | 87 ++++++++++--------- 1 file changed, 46 insertions(+), 41 deletions(-) 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 270afc86..73bb3391 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,7 +31,6 @@ 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; @@ -95,48 +94,54 @@ void generateReport_prefixedLogger_producesExpectedTaggedLayout() { 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(); + // Remove the diagnostic kind marker and the exact measurements (durations, percentages, + // counts) before comparing — the assertion pins the report's structure, labels and + // indentation, not the timings + String actual = + String.join( + "\n", + 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: s (%)", - "simple-builders: │ │ ├─ Constructors: s (%)", - "simple-builders: │ │ ├─ Methods: s (%)", - "simple-builders: │ │ ├─ Nested Types: s (%)", - "simple-builders: │ │ └─ Class Annotations: s (%)", - "simple-builders: │ ├─ String Generation: s (%)", - "simple-builders: │ └─ Formatting: s (%)", - "simple-builders: └─ File Writing: s (%)", - "simple-builders: ", - "simple-builders: Average per class: ms", - "simple-builders: ", - "simple-builders: Top slowest classes:", - "simple-builders: . MyClass - ms ( fields, collections)", - "simple-builders: ", - "simple-builders: Top slowest MethodGenerators:", - "simple-builders: . MyGenerator - s ( calls, ms/call)", - "simple-builders: ", - "simple-builders: Top slowest BuilderEnhancers:", - "simple-builders: . MyEnhancer - s ( calls, ms/call)"), + """ + simple-builders: PERFORMANCE REPORT + simple-builders: ================================ + simple-builders: Total classes processed: + simple-builders: Total processing time: s + simple-builders:\s + 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: s (%) + simple-builders: │ │ ├─ Constructors: s (%) + simple-builders: │ │ ├─ Methods: s (%) + simple-builders: │ │ ├─ Nested Types: s (%) + simple-builders: │ │ └─ Class Annotations: s (%) + simple-builders: │ ├─ String Generation: s (%) + simple-builders: │ └─ Formatting: s (%) + simple-builders: └─ File Writing: s (%) + simple-builders:\s + simple-builders: Average per class: ms + simple-builders:\s + simple-builders: Top slowest classes: + simple-builders: . MyClass - ms ( fields, collections) + simple-builders:\s + simple-builders: Top slowest MethodGenerators: + simple-builders: . MyGenerator - s ( calls, ms/call) + simple-builders:\s + simple-builders: Top slowest BuilderEnhancers: + simple-builders: . MyEnhancer - s ( calls, ms/call)\ + """, actual); } From 0ff60e3d7a4d11ae9e0404fa47bd2ae41321d3eb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:09:56 +0000 Subject: [PATCH 096/170] =?UTF-8?q?context:=20store=20the=20current=20task?= =?UTF-8?q?=20=E2=80=94=20ProcessingTargetPackage=20only=20re-wrapped=20wh?= =?UTF-8?q?at=20the=20task=20already=20carries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/AbstractCodegenProcessor.java | 7 +-- .../codegen/processing/CodegenTask.java | 5 +- .../processing/ProcessingTargetPackage.java | 41 ----------------- .../analysis/BuilderScopeResolver.java | 4 +- .../AnnotationProcessingContext.java | 31 ++++++------- .../processing/BuilderProcessingContext.java | 4 +- .../processor/BuilderScopeResolverTest.java | 46 +++++++++---------- 7 files changed, 46 insertions(+), 92 deletions(-) delete mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTargetPackage.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 1c51dca4..7a79b8a7 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -186,8 +186,7 @@ public final boolean process(Set annotations, RoundEnviro context.debugStartOperation("Processing element: " + task.element().getSimpleName()); tracker.startClass(task.element().getSimpleName().toString()); try { - context.initProcessingTargetPackage( - new ProcessingTargetPackage<>(task.configuration(), task.outputPackage())); + context.initProcessingTask(task); tracker.startPhase(); List definitions; try { @@ -221,9 +220,7 @@ public final boolean process(Set annotations, RoundEnviro context.debugStartOperation( "Generating targets for: " + work.task().element().getSimpleName()); try { - context.initProcessingTargetPackage( - new ProcessingTargetPackage<>( - work.task().configuration(), work.task().outputPackage())); + context.initProcessingTask(work.task()); 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 a4524082..2f8163e9 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java @@ -39,8 +39,9 @@ * @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 - * ProcessingTargetPackage} before analysis + * does not carry one — planning computes it so the driver can pass the task to {@link + * org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext#initProcessingTask} + * before analysis */ public record CodegenTask( Element element, K configuration, Element reportingElement, String outputPackage) {} diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTargetPackage.java b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTargetPackage.java deleted file mode 100644 index f3a3e603..00000000 --- a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessingTargetPackage.java +++ /dev/null @@ -1,41 +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; - -/** - * Per-target processing state for the type that is currently being generated. - * - *

{@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 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 - */ -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 e34749b8..3b3eae14 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 @@ -95,7 +95,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#initProcessingTargetPackage} + * org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext#initProcessingTask} * has been invoked with the owner element's resolved configuration beforehand, so that * per-element {@code builderUsagePackages} overrides are respected. * @@ -160,7 +160,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#initProcessingTargetPackage} + * org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext#initProcessingTask} * 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 329ef6b8..2a8f5f2c 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,7 +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.ProcessingTargetPackage; +import org.javahelpers.codegen.processing.CodegenTask; 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; @@ -81,8 +81,8 @@ public class AnnotationProcessingContext implements TypeMappingContext { */ private final GeneratedTypes generatedTypes = new GeneratedTypes(); - /** Per-target processing state for the type currently being generated. */ - private ProcessingTargetPackage processingTargetPackage; + /** The task currently being processed — set by the driver before analysis and generation. */ + private CodegenTask currentTask; /** * Creates the context with default options (strict mode enabled, performance tracking disabled). @@ -118,25 +118,23 @@ public AnnotationProcessingContext( } /** - * 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. + * Initializes the task currently being processed. 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 processingTargetPackage the resolved configuration and output package of the current - * target + * @param task the task currently being processed */ - public void initProcessingTargetPackage(ProcessingTargetPackage processingTargetPackage) { - this.processingTargetPackage = processingTargetPackage; + public void initProcessingTask(CodegenTask task) { + this.currentTask = task; } /** - * The state of the current processing target. + * The task currently being processed. * - * @return the current target's configuration and output package + * @return the current task with its element, configuration and output package */ - public ProcessingTargetPackage getProcessingTargetPackage() { - return processingTargetPackage; + public CodegenTask getCurrentTask() { + return currentTask; } /** @@ -159,8 +157,7 @@ public boolean isAccessibleFromGeneratedPackage(Element member) { if (member.getModifiers().contains(Modifier.PRIVATE)) { return false; } - return processingTargetPackage != null - && getPackageName(member).equals(processingTargetPackage.targetPackage()); + return currentTask != null && getPackageName(member).equals(currentTask.outputPackage()); } /** 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 b1a36463..2e8721b8 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) getProcessingTargetPackage().configuration(); + return (BuilderConfiguration) getCurrentTask().configuration(); } /** @@ -93,7 +93,7 @@ public BuilderConfiguration getConfiguration() { * @return the qualified package name of the generated builder */ public String getBuilderPackageName() { - return getProcessingTargetPackage().targetPackage(); + return getCurrentTask().outputPackage(); } /** 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 489f556d..0fd73055 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.ProcessingTargetPackage; +import org.javahelpers.codegen.processing.CodegenTask; 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,8 +1314,8 @@ public boolean process(Set annotations, RoundEnvironment BuilderProcessingContext context = new BuilderProcessingContext( ProcessingLogger.create(processingEnv), BuilderConfiguration.DEFAULT, processingEnv); - context.initProcessingTargetPackage( - new ProcessingTargetPackage(configuration("lib", "Builder"), "")); + context.initProcessingTask( + new CodegenTask<>(helper, configuration("lib", "Builder"), helper, "")); BuilderScopeResolver resolver = context.getBuilderScopeResolver(); // Register the type as generated, mirroring the real processor which calls // registerGeneratedBuilder before any resolution happens. @@ -1325,59 +1325,59 @@ public boolean process(Set annotations, RoundEnvironment second = resolver.resolveUsableBuilderType(helper); // Clear registration before testing scope-only behavior resolver.resetGeneratedBuilders(); - context.initProcessingTargetPackage( - new ProcessingTargetPackage(configuration("other", "OtherBuilder"), "")); + context.initProcessingTask( + new CodegenTask<>(helper, configuration("other", "OtherBuilder"), helper, "")); afterConfigurationChange = resolver.resolveUsableBuilderType(helper); - context.initProcessingTargetPackage( - new ProcessingTargetPackage(usageOnlyConfiguration("other"), "")); + context.initProcessingTask( + new CodegenTask<>(helper, usageOnlyConfiguration("other"), helper, "")); 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.initProcessingTargetPackage( - new ProcessingTargetPackage(usageOnlyConfiguration("lib"), "")); + context.initProcessingTask( + new CodegenTask<>(helper, usageOnlyConfiguration("lib"), helper, "")); 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.initProcessingTargetPackage( - new ProcessingTargetPackage(usageOnlyConfiguration("lib"), "")); + context.initProcessingTask( + new CodegenTask<>(helper, usageOnlyConfiguration("lib"), helper, "")); resolver.resetGeneratedBuilders(); usageWithoutAnnotation = resolver.resolveUsableBuilderType(helper); // Usage scope with builderUsageSuffix="Factory" - context.initProcessingTargetPackage( - new ProcessingTargetPackage(usageWithSuffixConfiguration("lib", "Factory"), "")); + context.initProcessingTask( + new CodegenTask<>(helper, usageWithSuffixConfiguration("lib", "Factory"), helper, "")); usageWithSuffix = resolver.resolveUsableBuilderType(helper); // Usage scope with default suffix (no builderUsageSuffix configured) - context.initProcessingTargetPackage( - new ProcessingTargetPackage(usageOnlyConfiguration("lib"), "")); + context.initProcessingTask( + new CodegenTask<>(helper, usageOnlyConfiguration("lib"), helper, "")); usageDefaultSuffix = resolver.resolveUsableBuilderType(helper); // Generated builder in the same package as the referenced builder: package-private // contract members are accessible - context.initProcessingTargetPackage( - new ProcessingTargetPackage(usageOnlyConfiguration("lib"), "lib")); + context.initProcessingTask( + new CodegenTask<>(helper, usageOnlyConfiguration("lib"), helper, "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.initProcessingTargetPackage( - new ProcessingTargetPackage(existingBuildersDisabledConfiguration("lib"), "")); + context.initProcessingTask( + new CodegenTask<>(helper, existingBuildersDisabledConfiguration("lib"), helper, "")); 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.initProcessingTargetPackage( - new ProcessingTargetPackage(prefillDisabledConfiguration("lib"), "")); + context.initProcessingTask( + new CodegenTask<>(helper, prefillDisabledConfiguration("lib"), helper, "")); 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.initProcessingTargetPackage( - new ProcessingTargetPackage(usageOnlyConfiguration("java.lang"), "")); + context.initProcessingTask( + new CodegenTask<>(helper, usageOnlyConfiguration("java.lang"), helper, "")); resolver.resetGeneratedBuilders(); TypeElement stringElement = processingEnv.getElementUtils().getTypeElement("java.lang.String"); From 05864fafdf21442f5e99f2fb978bc51667491dc0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:16:20 +0000 Subject: [PATCH 097/170] =?UTF-8?q?context:=20initProcessingTask=20->=20se?= =?UTF-8?q?tCurrentTask=20=E2=80=94=20it=20is=20called=20per=20task,=20a?= =?UTF-8?q?=20setter=20not=20an=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/AbstractCodegenProcessor.java | 4 +-- .../codegen/processing/CodegenTask.java | 2 +- .../analysis/BuilderScopeResolver.java | 4 +-- .../AnnotationProcessingContext.java | 8 +++--- .../processor/BuilderScopeResolverTest.java | 25 ++++++++----------- 5 files changed, 20 insertions(+), 23 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 7a79b8a7..f9c68764 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -186,7 +186,7 @@ public final boolean process(Set annotations, RoundEnviro context.debugStartOperation("Processing element: " + task.element().getSimpleName()); tracker.startClass(task.element().getSimpleName().toString()); try { - context.initProcessingTask(task); + context.setCurrentTask(task); tracker.startPhase(); List definitions; try { @@ -220,7 +220,7 @@ public final boolean process(Set annotations, RoundEnviro context.debugStartOperation( "Generating targets for: " + work.task().element().getSimpleName()); try { - context.initProcessingTask(work.task()); + context.setCurrentTask(work.task()); 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 2f8163e9..481895f6 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 pass the task to {@link - * org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext#initProcessingTask} + * org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext#setCurrentTask} * before analysis */ public record CodegenTask( 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 3b3eae14..1197eda9 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 @@ -95,7 +95,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#initProcessingTask} + * org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext#setCurrentTask} * has been invoked with the owner element's resolved configuration beforehand, so that * per-element {@code builderUsagePackages} overrides are respected. * @@ -160,7 +160,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#initProcessingTask} + * org.javahelpers.simple.builders.processor.processing.BuilderProcessingContext#setCurrentTask} * 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 2a8f5f2c..83af5152 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 @@ -118,13 +118,13 @@ public AnnotationProcessingContext( } /** - * Initializes the task currently being processed. 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. + * Sets the task currently being processed. 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 task the task currently being processed */ - public void initProcessingTask(CodegenTask task) { + public void setCurrentTask(CodegenTask task) { this.currentTask = task; } 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 0fd73055..fe8b62fa 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 @@ -1314,7 +1314,7 @@ public boolean process(Set annotations, RoundEnvironment BuilderProcessingContext context = new BuilderProcessingContext( ProcessingLogger.create(processingEnv), BuilderConfiguration.DEFAULT, processingEnv); - context.initProcessingTask( + context.setCurrentTask( new CodegenTask<>(helper, configuration("lib", "Builder"), helper, "")); BuilderScopeResolver resolver = context.getBuilderScopeResolver(); // Register the type as generated, mirroring the real processor which calls @@ -1325,10 +1325,10 @@ public boolean process(Set annotations, RoundEnvironment second = resolver.resolveUsableBuilderType(helper); // Clear registration before testing scope-only behavior resolver.resetGeneratedBuilders(); - context.initProcessingTask( + context.setCurrentTask( new CodegenTask<>(helper, configuration("other", "OtherBuilder"), helper, "")); afterConfigurationChange = resolver.resolveUsableBuilderType(helper); - context.initProcessingTask( + context.setCurrentTask( new CodegenTask<>(helper, usageOnlyConfiguration("other"), helper, "")); beforeRegistration = resolver.resolveUsableBuilderType(helper); // Registration alone is not enough — the type must be in scope @@ -1336,47 +1336,44 @@ public boolean process(Set annotations, RoundEnvironment new TypeName("lib", "LibHelper"), new TypeName("lib", "LibHelperBuilder")); afterRegistration = resolver.resolveUsableBuilderType(helper); // Clear registration for usage-scope classpath lookup tests - context.initProcessingTask( - new CodegenTask<>(helper, usageOnlyConfiguration("lib"), helper, "")); + context.setCurrentTask(new CodegenTask<>(helper, usageOnlyConfiguration("lib"), helper, "")); 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.initProcessingTask( - new CodegenTask<>(helper, usageOnlyConfiguration("lib"), helper, "")); + context.setCurrentTask(new CodegenTask<>(helper, usageOnlyConfiguration("lib"), helper, "")); resolver.resetGeneratedBuilders(); usageWithoutAnnotation = resolver.resolveUsableBuilderType(helper); // Usage scope with builderUsageSuffix="Factory" - context.initProcessingTask( + context.setCurrentTask( new CodegenTask<>(helper, usageWithSuffixConfiguration("lib", "Factory"), helper, "")); usageWithSuffix = resolver.resolveUsableBuilderType(helper); // Usage scope with default suffix (no builderUsageSuffix configured) - context.initProcessingTask( - new CodegenTask<>(helper, usageOnlyConfiguration("lib"), helper, "")); + context.setCurrentTask(new CodegenTask<>(helper, usageOnlyConfiguration("lib"), helper, "")); usageDefaultSuffix = resolver.resolveUsableBuilderType(helper); // Generated builder in the same package as the referenced builder: package-private // contract members are accessible - context.initProcessingTask( + context.setCurrentTask( new CodegenTask<>(helper, usageOnlyConfiguration("lib"), helper, "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.initProcessingTask( + context.setCurrentTask( new CodegenTask<>(helper, existingBuildersDisabledConfiguration("lib"), helper, "")); 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.initProcessingTask( + context.setCurrentTask( new CodegenTask<>(helper, prefillDisabledConfiguration("lib"), helper, "")); 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.initProcessingTask( + context.setCurrentTask( new CodegenTask<>(helper, usageOnlyConfiguration("java.lang"), helper, "")); resolver.resetGeneratedBuilders(); TypeElement stringElement = From ac4f49b3fb2a2f9fba78a5bb002005ad8f4809ff Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:10:52 +0000 Subject: [PATCH 098/170] =?UTF-8?q?javadoc:=20re-attach=20dangling=20docs?= =?UTF-8?q?=20=E2=80=94=20orphan=20comments=20removed,=20codeql=20tags=20m?= =?UTF-8?q?oved=20onto=20the=20signature=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/AbstractCodegenProcessor.java | 35 +++++++------------ 1 file changed, 13 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 f9c68764..35522eeb 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -91,10 +91,6 @@ public abstract class AbstractCodegenProcessor< D extends CodegenDefinition> extends AbstractProcessor { - /** Performance phase the element collection is reported under — framework-owned. */ - - /** Performance phase target generation is reported under — framework-owned. */ - /** The domain's context instance. */ protected C context; @@ -417,8 +413,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) { + protected boolean claimsAnnotations(Set supportedTypes) { // codeql[java/unused-parameter] return false; } @@ -522,8 +517,8 @@ 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); + protected abstract List plannedTaskResultTypeNames( + CodegenTask task, C context); // codeql[java/unused-parameter] /** * Runs after a predicted type name is registered for a task — the extension point for domain @@ -533,8 +528,8 @@ protected void registerPlannedTaskResults(List> tasks, C context) * @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 plannedName, C context) { + protected void onTaskResultRegistered( + CodegenTask task, TypeName plannedName, C context) { // codeql[java/unused-parameter] // Extension point for domains — registration bookkeeping is already done by the driver. } @@ -545,8 +540,7 @@ protected void onTaskResultRegistered(CodegenTask task, TypeName plannedName, * * @param context the processing context */ - // codeql[java/unused-parameter] — extension point; domains may need every parameter. - protected void onRoundStart(C context) { + protected void onRoundStart(C context) { // codeql[java/unused-parameter] // Extension point for domains — the driver performs no per-round setup beyond logging. } @@ -558,8 +552,8 @@ 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> tasks, C context) { + protected void onTasksPlanned( + List> tasks, C context) { // codeql[java/unused-parameter] // Extension point for domains — planning bookkeeping is already done by the driver. } @@ -572,8 +566,7 @@ protected void onTasksPlanned(List> 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) { + protected boolean isFinished(C context) { // codeql[java/unused-parameter] return processedOnce; } @@ -610,8 +603,8 @@ 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) { + protected void onTaskGenerated( + PlannedGeneration generation, C context) { // codeql[java/unused-parameter] // Extension point for domains — the target is already generated at this point. } @@ -621,8 +614,7 @@ 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) { + protected void onProcessingOver(C context) { // codeql[java/unused-parameter] // Extension point for domains — e.g. last-round outputs via {@link #generateTarget}. } @@ -631,8 +623,7 @@ protected void onProcessingOver(C context) { * * @param context the processing context */ - // codeql[java/unused-parameter] — extension point; domains may need every parameter. - protected void endRound(C context) { + protected void endRound(C context) { // codeql[java/unused-parameter] // Extension point for domains — e.g. per-round finalization. } From 1a7ed2d72ec99785ad2b0db6b55c471cf7b996ff Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:11:13 +0000 Subject: [PATCH 099/170] =?UTF-8?q?driver:=20drop=20the=20wildcard=20in=20?= =?UTF-8?q?onTasksPlanned=20=E2=80=94=20the=20list=20is=20already=20Codege?= =?UTF-8?q?nTask?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codegen/processing/AbstractCodegenProcessor.java | 2 +- 1 file changed, 1 insertion(+), 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 35522eeb..514426f5 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -553,7 +553,7 @@ protected void onRoundStart(C context) { // codeql[java/unused-parameter] * @param context the processing context */ protected void onTasksPlanned( - List> tasks, C context) { // codeql[java/unused-parameter] + List> tasks, C context) { // codeql[java/unused-parameter] // Extension point for domains — planning bookkeeping is already done by the driver. } From 4a6ed4bbef39d22765185fa174ff4c1196c01378 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:11:17 +0000 Subject: [PATCH 100/170] =?UTF-8?q?driver:=20rename=20local=20logger=20in?= =?UTF-8?q?=20createLogger=20=E2=80=94=20it=20shadowed=20the=20processor's?= =?UTF-8?q?=20logger=20field?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codegen/processing/AbstractCodegenProcessor.java | 6 +++--- 1 file changed, 3 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 514426f5..a0bb2e72 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -335,12 +335,12 @@ protected String provideLoggerPrefix() { private ProcessingLogger createLogger( ProcessingEnvironment processingEnv, CompilerArgumentsReader reader) { String loggerPrefix = provideLoggerPrefix(); - ProcessingLogger logger = + ProcessingLogger processingLogger = optionRegistry().isVerboseRequested(reader) ? ProcessingLogger.createVerbose(processingEnv, loggerPrefix) : ProcessingLogger.create(processingEnv, loggerPrefix); - logger.debug("Starting %s...", getClass().getSimpleName()); - return logger; + processingLogger.debug("Starting %s...", getClass().getSimpleName()); + return processingLogger; } /** From f095f57910d44640ccf1818560a2f5c8b32c4b7c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:11:20 +0000 Subject: [PATCH 101/170] =?UTF-8?q?driver:=20suppress=20S112=20on=20analys?= =?UTF-8?q?e/map=20=E2=80=94=20extension=20points=20cannot=20enumerate=20d?= =?UTF-8?q?omain=20failures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codegen/processing/AbstractCodegenProcessor.java | 2 ++ 1 file changed, 2 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 a0bb2e72..cabdac0c 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -580,6 +580,7 @@ protected boolean isFinished(C context) { // codeql[java/unused-parameter] * @return the domain definitions, possibly empty * @throws Exception when analysis fails; reported as a per-element failure */ + @SuppressWarnings("java:S112") // extension point — the domain cannot enumerate its failures protected abstract List analyse(CodegenTask task, C context) throws Exception; /** @@ -593,6 +594,7 @@ protected boolean isFinished(C context) { // codeql[java/unused-parameter] * @return the class definition to generate * @throws Exception when mapping fails; reported as a per-element failure */ + @SuppressWarnings("java:S112") // extension point — the domain cannot enumerate its failures protected abstract GenerationTargetClassDto mapToTargetSpecification( D definition, K configuration, C context) throws Exception; From 338767b82bcd3eb7abeba2f7a489e42132e2aea1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:11:24 +0000 Subject: [PATCH 102/170] =?UTF-8?q?context:=20suppress=20S1452=20on=20getC?= =?UTF-8?q?urrentTask=20=E2=80=94=20the=20task's=20config=20type=20varies?= =?UTF-8?q?=20per=20domain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processor/processing/AnnotationProcessingContext.java | 1 + 1 file changed, 1 insertion(+) 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 83af5152..55f280ce 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 @@ -133,6 +133,7 @@ public void setCurrentTask(CodegenTask task) { * * @return the current task with its element, configuration and output package */ + @SuppressWarnings("java:S1452") // the task's configuration type varies per domain public CodegenTask getCurrentTask() { return currentTask; } From 28614949cb34ce879fc89ec878d0ae060e422911 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:11:24 +0000 Subject: [PATCH 103/170] =?UTF-8?q?processor:=20globalConfig=20is=20only?= =?UTF-8?q?=20used=20inside=20createContext=20=E2=80=94=20make=20it=20a=20?= =?UTF-8?q?local?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../simple/builders/processor/BuilderProcessor.java | 6 ++---- 1 file changed, 2 insertions(+), 4 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 bf981aa2..3fa125f8 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 @@ -134,9 +134,6 @@ public BuilderProcessor() { super(PROCESSOR_CONFIGURATION); } - /** 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. */ @@ -156,7 +153,8 @@ protected BuilderProcessingContext createContext( ProcessingEnvironment processingEnv, CompilerArgumentsReader reader, ProcessingLogger logger) { - this.globalConfig = BuilderConfigurationReader.readCompilerConfiguration(reader, logger); + BuilderConfiguration globalConfig = + BuilderConfigurationReader.readCompilerConfiguration(reader, logger); logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig); SimpleBuildersSpiIntegration.initCompilation(processingEnv.getElementUtils()); this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig); From 55679dac7e6e896d28419b9d8ef088a98c17d08d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:11:24 +0000 Subject: [PATCH 104/170] analyser: List -> List in findAnnotationAttributeArray return type --- .../processor/analysis/JavaLangAnalyser.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 f6abc825..28679afe 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,15 +322,15 @@ 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 findAnnotationAttributeArray( + public static List findAnnotationAttributeArray( 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()); + Object value = + findAnnotationAttribute(annotationMirror, attributeName, context) + .map(AnnotationValue::getValue) + .orElse(null); + return value instanceof List list ? new ArrayList<>(list) : List.of(); } /** From d8bcff7e89575cd9acff7b60d85aa799e2feb6b3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:17:03 +0000 Subject: [PATCH 105/170] =?UTF-8?q?test:=20cover=20flagged=20new=20lines?= =?UTF-8?q?=20=E2=80=94=20config=20defaults,=20registry=20null=20paths,=20?= =?UTF-8?q?non-annotation=20skip,=20unresolvable=20SimpleBuilderFor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CodegenProcessorConfigurationTest.java | 49 ++++++ .../processing/ElementCollectorTest.java | 145 ++++++++++++++++++ .../ProcessorOptionRegistryTest.java | 49 ++++++ .../processor/SimpleBuilderForTest.java | 16 ++ 4 files changed, 259 insertions(+) create mode 100644 processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java create mode 100644 processor/src/test/java/org/javahelpers/codegen/processing/ElementCollectorTest.java create mode 100644 processor/src/test/java/org/javahelpers/codegen/processing/ProcessorOptionRegistryTest.java diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java new file mode 100644 index 00000000..da8e8f48 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java @@ -0,0 +1,49 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link CodegenProcessorConfiguration}. */ +class CodegenProcessorConfigurationTest { + + @Test + void build_withoutElementCollector_throwsIllegalState() { + assertThrows( + IllegalStateException.class, () -> CodegenProcessorConfiguration.builder().build()); + } + + @Test + void formatterFactory_defaultFormatsSourceUnchanged() { + CodegenProcessorConfiguration config = + CodegenProcessorConfiguration.builder() + .elementCollector(ElementCollector.builder().build()) + .build(); + + assertEquals("raw", config.formatterFactory().create(null, null, null).format("raw")); + } +} diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/ElementCollectorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/ElementCollectorTest.java new file mode 100644 index 00000000..83793514 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/codegen/processing/ElementCollectorTest.java @@ -0,0 +1,145 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.*; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Set; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.type.TypeMirror; +import org.javahelpers.simple.builders.processor.testing.CapturingProcessingLogger; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ElementCollector}. */ +class ElementCollectorTest { + + @Test + void collect_nonAnnotationTypeElementsAreSkipped() { + ElementCollector collector = + ElementCollector.builder().selector(selector -> selector.groupName("group")).build(); + + // a javac round only ever passes annotation types — a non-annotation entry is skipped early + ElementGroups groups = + collector.collect( + Set.of(new StubTypeElement(ElementKind.CLASS)), + null, + CapturingProcessingLogger.create().logger()); + + assertTrue(groups.byGroup("group").isEmpty()); + } + + private static class StubTypeElement implements TypeElement { + private final ElementKind kind; + + StubTypeElement(ElementKind kind) { + this.kind = kind; + } + + @Override + public ElementKind getKind() { + return kind; + } + + @Override + public TypeMirror asType() { + return null; + } + + @Override + public Set getModifiers() { + return Set.of(); + } + + @Override + public Name getSimpleName() { + return null; + } + + @Override + public Element getEnclosingElement() { + return null; + } + + @Override + public List getEnclosedElements() { + return List.of(); + } + + @Override + public List getAnnotationMirrors() { + return List.of(); + } + + @Override + public A getAnnotation(Class annotationType) { + return null; + } + + @Override + public A[] getAnnotationsByType(Class annotationType) { + return null; + } + + @Override + public R accept(ElementVisitor visitor, P parameter) { + return null; + } + + @Override + public List getTypeParameters() { + return List.of(); + } + + @Override + public TypeMirror getSuperclass() { + return null; + } + + @Override + public List getInterfaces() { + return List.of(); + } + + @Override + public NestingKind getNestingKind() { + return NestingKind.TOP_LEVEL; + } + + @Override + public Name getQualifiedName() { + return null; + } + } +} diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/ProcessorOptionRegistryTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/ProcessorOptionRegistryTest.java new file mode 100644 index 00000000..dc0aed7d --- /dev/null +++ b/processor/src/test/java/org/javahelpers/codegen/processing/ProcessorOptionRegistryTest.java @@ -0,0 +1,49 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.*; + +import java.util.Set; +import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link ProcessorOptionRegistry}. */ +class ProcessorOptionRegistryTest { + + private static ProcessorOptionRegistry minimalRegistry() { + return ProcessorOptionRegistry.builder().options(Set.of()).build(); + } + + @Test + void reader_returnsReaderForEnvironment() { + assertNotNull(minimalRegistry().reader(null)); + } + + @Test + void formatterProfile_withoutConfiguredOption_returnsNull() { + assertNull(minimalRegistry().formatterProfile(new CompilerArgumentsReader(null))); + } +} 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 f4eae973..8a412fb9 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 @@ -754,6 +754,22 @@ public BaseDto() {} """); } + @Test + void sourcePackages_NoResolvableTypes_Warns() { + JavaFileObject holder = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor; + @SimpleBuilderFor(sourcePackages = "nonexistent.pkg") + public class Builders {} + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(holder); + + assertThat(compilation).hadWarningContaining("resolves no types - nothing to generate"); + } + private static JavaFileObject holder(String pkg, String className, String targetType) { return ProcessorTestUtils.forSource( """ From be948087ceefacd1ec16eaf4c0e878ace822d9bb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:58:19 +0000 Subject: [PATCH 106/170] model: @Override on getTypeName (CodeQL java/missing-override-annotation) --- .../builders/processor/model/core/BuilderDefinitionDto.java | 1 + 1 file changed, 1 insertion(+) 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 6876b2b8..74de1924 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 @@ -139,6 +139,7 @@ public void setBuilderTypeName(TypeName builderClassName) { this.typeName = builderClassName; } + @Override public TypeName getTypeName() { return getBuilderTypeName(); } From 3b830d5dbe74e045fbde89893febb6b36bbc2e91 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:58:19 +0000 Subject: [PATCH 107/170] test: builder created outside assertThrows lambda (Sonar S5778) --- .../codegen/processing/CodegenProcessorConfigurationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java index da8e8f48..1e62b231 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java @@ -33,8 +33,8 @@ class CodegenProcessorConfigurationTest { @Test void build_withoutElementCollector_throwsIllegalState() { - assertThrows( - IllegalStateException.class, () -> CodegenProcessorConfiguration.builder().build()); + CodegenProcessorConfiguration.Builder builder = CodegenProcessorConfiguration.builder(); + assertThrows(IllegalStateException.class, builder::build); } @Test From 5a9f357142e7a6dc5df0eb11c420214dbbb23112 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:00:14 +0000 Subject: [PATCH 108/170] =?UTF-8?q?processing:=20drop=20trailing=20codeql?= =?UTF-8?q?=20comments=20=E2=80=94=20not=20recognized;=20alerts=20dismisse?= =?UTF-8?q?d=20in=20Security=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/AbstractCodegenProcessor.java | 22 ++++++++----------- 1 file changed, 9 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 cabdac0c..e66f8708 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -413,7 +413,7 @@ private ProcessorOptionRegistry optionRegistry() { * @param supportedTypes the supported annotation type names the round was processed for * @return {@code true} to claim the supported annotations */ - protected boolean claimsAnnotations(Set supportedTypes) { // codeql[java/unused-parameter] + protected boolean claimsAnnotations(Set supportedTypes) { return false; } @@ -517,8 +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) */ - protected abstract List plannedTaskResultTypeNames( - CodegenTask task, C context); // codeql[java/unused-parameter] + protected abstract List plannedTaskResultTypeNames(CodegenTask task, C context); /** * Runs after a predicted type name is registered for a task — the extension point for domain @@ -528,8 +527,7 @@ protected abstract List plannedTaskResultTypeNames( * @param plannedName the predicted type name just registered for it * @param context the processing context */ - protected void onTaskResultRegistered( - CodegenTask task, TypeName plannedName, C context) { // codeql[java/unused-parameter] + protected void onTaskResultRegistered(CodegenTask task, TypeName plannedName, C context) { // Extension point for domains — registration bookkeeping is already done by the driver. } @@ -540,7 +538,7 @@ protected void onTaskResultRegistered( * * @param context the processing context */ - protected void onRoundStart(C context) { // codeql[java/unused-parameter] + protected void onRoundStart(C context) { // Extension point for domains — the driver performs no per-round setup beyond logging. } @@ -552,8 +550,7 @@ protected void onRoundStart(C context) { // codeql[java/unused-parameter] * @param tasks the tasks the domain's {@link #planTasks} produced this round * @param context the processing context */ - protected void onTasksPlanned( - List> tasks, C context) { // codeql[java/unused-parameter] + protected void onTasksPlanned(List> tasks, C context) { // Extension point for domains — planning bookkeeping is already done by the driver. } @@ -566,7 +563,7 @@ protected void onTasksPlanned( * @param context the processing context * @return {@code true} to skip this round */ - protected boolean isFinished(C context) { // codeql[java/unused-parameter] + protected boolean isFinished(C context) { return processedOnce; } @@ -605,8 +602,7 @@ 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) { // codeql[java/unused-parameter] + protected void onTaskGenerated(PlannedGeneration generation, C context) { // Extension point for domains — the target is already generated at this point. } @@ -616,7 +612,7 @@ protected void onTaskGenerated( * * @param context the processing context */ - protected void onProcessingOver(C context) { // codeql[java/unused-parameter] + protected void onProcessingOver(C context) { // Extension point for domains — e.g. last-round outputs via {@link #generateTarget}. } @@ -625,7 +621,7 @@ protected void onProcessingOver(C context) { // codeql[java/unused-parameter] * * @param context the processing context */ - protected void endRound(C context) { // codeql[java/unused-parameter] + protected void endRound(C context) { // Extension point for domains — e.g. per-round finalization. } From 5c346360b676f2d3b9d9e64638bdcd5b584fac77 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:11:11 +0000 Subject: [PATCH 109/170] =?UTF-8?q?processing:=20drop=20CodegenConfigurati?= =?UTF-8?q?on=20=E2=80=94=20re-added=20below=20as=20a=20new=20file=20so=20?= =?UTF-8?q?it=20does=20not=20carry=20ProcessingTarget=20lineage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/CodegenConfiguration.java | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java deleted file mode 100644 index 8894fa4b..00000000 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java +++ /dev/null @@ -1,50 +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; - -/** - * 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(); -} From 13052a38179387719cdcd481b64fe1acebff5623 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:11:11 +0000 Subject: [PATCH 110/170] =?UTF-8?q?processing:=20restore=20ProcessingTarge?= =?UTF-8?q?t=20from=20main=20=E2=80=94=20keeps=20its=20file=20history?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/ProcessingTarget.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java new file mode 100644 index 00000000..f33d0478 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java @@ -0,0 +1,43 @@ +/* + * 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.processing; + +import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; + +/** + * Per-target processing state for the type whose builder 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. + * + * @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 + */ +public record ProcessingTarget(BuilderConfiguration configuration, String builderPackage) {} From 46ccbd520879374a3eafc0f538e7018812a45686 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:11:11 +0000 Subject: [PATCH 111/170] =?UTF-8?q?processing:=20drop=20CodegenTask=20?= =?UTF-8?q?=E2=80=94=20re-created=20from=20ProcessingTarget=20below?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codegen/processing/CodegenTask.java | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java deleted file mode 100644 index 481895f6..00000000 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java +++ /dev/null @@ -1,47 +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 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 - * @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 pass the task to {@link - * org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext#setCurrentTask} - * before analysis - */ -public record CodegenTask( - Element element, K configuration, Element reportingElement, String outputPackage) {} From e8e03b122f33fe2a433fc0e5e3aa6c1331e113e8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:11:11 +0000 Subject: [PATCH 112/170] =?UTF-8?q?processing:=20git=20mv=20ProcessingTarg?= =?UTF-8?q?et=20to=20CodegenTask=20=E2=80=94=20CodegenTask=20is=20its=20su?= =?UTF-8?q?ccessor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProcessingTarget.java => codegen/processing/CodegenTask.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename processor/src/main/java/org/javahelpers/{simple/builders/processor/processing/ProcessingTarget.java => codegen/processing/CodegenTask.java} (100%) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java similarity index 100% rename from processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingTarget.java rename to processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java From 38037c6a27f886c85e26d136f7b1354b7755dce9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:11:11 +0000 Subject: [PATCH 113/170] processing: CodegenTask generalizes the per-target state for any codegen domain --- .../codegen/processing/CodegenTask.java | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) 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 f33d0478..481895f6 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java @@ -22,22 +22,26 @@ * SOFTWARE. */ -package org.javahelpers.simple.builders.processor.processing; +package org.javahelpers.codegen.processing; -import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; +import javax.lang.model.element.Element; /** - * Per-target processing state for the type whose builder is currently being generated. + * A planned generation: one element to generate code for, with the configuration the domain + * resolved during planning. * - *

{@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. + *

One round element may expand into several tasks (e.g. an annotation declaring multiple + * external types). * - * @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 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 + * @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 pass the task to {@link + * org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext#setCurrentTask} + * before analysis */ -public record ProcessingTarget(BuilderConfiguration configuration, String builderPackage) {} +public record CodegenTask( + Element element, K configuration, Element reportingElement, String outputPackage) {} From d321dff20b43856f90b7d6f8de9b1ce902afe16e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:11:11 +0000 Subject: [PATCH 114/170] =?UTF-8?q?processing:=20add=20CodegenConfiguratio?= =?UTF-8?q?n=20=E2=80=94=20common=20strict/verbose=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/CodegenConfiguration.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java 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(); +} From e013723f93f2747eb6555bce3acdfbfed411756b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:09:36 +0000 Subject: [PATCH 115/170] =?UTF-8?q?processing:=20matchingAnnotations=20ear?= =?UTF-8?q?ly-out=20when=20no=20metaMarker=20=E2=80=94=20guard=20beats=20a?= =?UTF-8?q?=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../javahelpers/codegen/processing/ElementCollector.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 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 5d091a98..cf95ce77 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java @@ -136,6 +136,10 @@ public ElementGroups collect( private static List matchingAnnotations( Set annotations, ElementSelector selector) { + Class metaMarker = selector.metaMarker(); + if (metaMarker == null) { + return List.of(); + } List result = new ArrayList<>(); for (TypeElement annotation : annotations) { if (annotation.getKind() != ElementKind.ANNOTATION_TYPE) { @@ -143,9 +147,7 @@ private static List matchingAnnotations( } // 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 (metaMarked) { + if (annotation.getAnnotation(metaMarker) != null) { result.add(annotation); } } From f023c4f3f073c0db6d286742268dbc0e36ce3c0e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:09:36 +0000 Subject: [PATCH 116/170] =?UTF-8?q?processing:=20drop=20FormatterFactory?= =?UTF-8?q?=20=E2=80=94=20SourceFormatterResolver=20creates=20its=20format?= =?UTF-8?q?ter=20directly=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/AbstractCodegenProcessor.java | 9 ++---- .../CodegenProcessorConfiguration.java | 23 ++------------- .../builders/processor/BuilderProcessor.java | 2 -- .../processing/SourceFormatterResolver.java | 28 ++++--------------- .../CodegenProcessorConfigurationTest.java | 10 ------- .../RoasterCodeGeneratorResilienceTest.java | 7 ++--- 6 files changed, 13 insertions(+), 66 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 e66f8708..31db71bb 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -382,8 +382,8 @@ 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. + * Creates the source formatter resolver for the configured profile — the profile name comes from + * the processor's declared options. * * @param reader the compiler arguments reader * @param logger the processing logger @@ -391,10 +391,7 @@ protected GenerationEnvironment generationEnvironment( */ protected SourceFormatterResolver createFormatterResolver( CompilerArgumentsReader reader, ProcessingLogger logger) { - return new SourceFormatterResolver( - logger, - optionRegistry().formatterProfile(reader), - processorConfiguration.formatterFactory()); + return new SourceFormatterResolver(logger, optionRegistry().formatterProfile(reader)); } private ElementCollector 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 dd5fe684..b4892b84 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java @@ -25,8 +25,6 @@ package org.javahelpers.codegen.processing; 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 @@ -39,14 +37,11 @@ * @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, - SourceFormatterResolver.FormatterFactory formatterFactory) { + String requiredSourceVersion) { /** * Creates a builder with all defaults applied — only {@link @@ -63,8 +58,6 @@ 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() {} @@ -104,18 +97,6 @@ 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. * @@ -127,7 +108,7 @@ public CodegenProcessorConfiguration build() { throw new IllegalStateException("elementCollector is required"); } return new CodegenProcessorConfiguration( - elementCollector, optionRegistry, requiredSourceVersion, formatterFactory); + elementCollector, 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 3fa125f8..4fe9933f 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 @@ -61,7 +61,6 @@ import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator; 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; @@ -126,7 +125,6 @@ public class BuilderProcessor .elementCollector(ELEMENT_COLLECTOR) .optionRegistry(OPTION_REGISTRY) .requiredSourceVersion("RELEASE_17") - .formatterFactory(RoasterSourceFormatter::new) .build(); /** Creates the processor — javac instantiates it reflectively. */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java index a3c117d9..a619ecdd 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java @@ -27,33 +27,19 @@ import java.util.EnumMap; 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 formatter profile the - * domain configured. + * Resolves the {@link SourceFormatter} for a formatting mode, applying the formatter profile + * configured via the {@code formatterProfile} compiler argument. * *

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); @@ -62,13 +48,10 @@ public interface FormatterFactory { * * @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( - ProcessingLogger logger, String formatterProfile, FormatterFactory factory) { + public SourceFormatterResolver(ProcessingLogger logger, String formatterProfile) { this.logger = logger; this.formatterProfile = formatterProfile; - this.factory = factory; } /** @@ -78,6 +61,7 @@ public SourceFormatterResolver( * @return a cached or new formatter instance */ public SourceFormatter resolve(FormattingMode mode) { - return formatterCache.computeIfAbsent(mode, m -> factory.create(logger, m, formatterProfile)); + return formatterCache.computeIfAbsent( + mode, m -> new RoasterSourceFormatter(logger, m, formatterProfile)); } } diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java index 1e62b231..973b1823 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java @@ -36,14 +36,4 @@ void build_withoutElementCollector_throwsIllegalState() { CodegenProcessorConfiguration.Builder builder = CodegenProcessorConfiguration.builder(); assertThrows(IllegalStateException.class, builder::build); } - - @Test - void formatterFactory_defaultFormatsSourceUnchanged() { - CodegenProcessorConfiguration config = - CodegenProcessorConfiguration.builder() - .elementCollector(ElementCollector.builder().build()) - .build(); - - assertEquals("raw", config.formatterFactory().create(null, null, null).format("raw")); - } } 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 85f9b3f0..379f07ae 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 @@ -34,7 +34,6 @@ 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; @@ -94,8 +93,7 @@ void shouldWrapRenderingRuntimeExceptionInCodeGenerationException() { env, logger, context.getPerformanceTracker(), context::reportBasedOnStrictMode)); SourceFormatter formatter = - new SourceFormatterResolver(logger, null, RoasterSourceFormatter::new) - .resolve(FormattingMode.NONE); + new SourceFormatterResolver(logger, null).resolve(FormattingMode.NONE); CodeGenerationException thrown = assertThrows( CodeGenerationException.class, () -> generator.generateClass(classDef, formatter)); @@ -131,8 +129,7 @@ void shouldKeepFirstMethodAndWarnOnDuplicateSignature() { context::reportBasedOnStrictMode)); SourceFormatter formatter = - new SourceFormatterResolver(capturing.logger(), null, RoasterSourceFormatter::new) - .resolve(FormattingMode.NONE); + new SourceFormatterResolver(capturing.logger(), null).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 c12da7aecc7eee61490b7c10f64edb0d17093217 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:21:49 +0000 Subject: [PATCH 117/170] processing: javadoc for the two element-group names --- .../simple/builders/processor/BuilderProcessor.java | 3 +++ 1 file changed, 3 insertions(+) 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 4fe9933f..6e378d0a 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 @@ -87,7 +87,10 @@ public class BuilderProcessor extends AbstractCodegenProcessor< BuilderProcessingContext, BuilderConfiguration, BuilderDefinitionDto> { + /** Group of elements carrying {@code @SimpleBuilder} or a template-meta-annotated annotation. */ private static final String GROUP_ANNOTATED_ELEMENTS = "annotatedElements"; + + /** Group of holder classes declaring external types via {@code @SimpleBuilderFor}. */ private static final String GROUP_EXTERNAL_TYPE_HOLDERS = "externalTypeHolders"; /** The processor's declared compiler options — the supported set is derived, not duplicated. */ From 3b025ce521f6818df93fe8b92a8544076d15677a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:22:06 +0000 Subject: [PATCH 118/170] processing: loggerPrefix is a static declaration on CodegenProcessorConfiguration --- .../processing/AbstractCodegenProcessor.java | 7 ++++--- .../CodegenProcessorConfiguration.java | 20 +++++++++++++++++-- .../builders/processor/BuilderProcessor.java | 7 +------ 3 files changed, 23 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 31db71bb..2b3398d5 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -319,13 +319,14 @@ private void assertTargetDoesNotExist(GenerationTargetClassDto target) /** * 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. + * logger to each message). Default: the {@link CodegenProcessorConfiguration#loggerPrefix()} + * declaration, or the processor's simple class name when unset; override for a dynamic prefix. * * @return the logger prefix */ protected String provideLoggerPrefix() { - return getClass().getSimpleName(); + String configured = processorConfiguration.loggerPrefix(); + return configured != null ? configured : getClass().getSimpleName(); } /** 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 b4892b84..61fc2147 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenProcessorConfiguration.java @@ -37,11 +37,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 loggerPrefix the diagnostic prefix carried by every emitted log line ({@code "prefix: "}); + * default: the processor's simple class name */ public record CodegenProcessorConfiguration( ElementCollector elementCollector, ProcessorOptionRegistry optionRegistry, - String requiredSourceVersion) { + String requiredSourceVersion, + String loggerPrefix) { /** * Creates a builder with all defaults applied — only {@link @@ -58,6 +61,7 @@ public static final class Builder { private ElementCollector elementCollector; private ProcessorOptionRegistry optionRegistry = new ProcessorOptionRegistry(List.of()); private String requiredSourceVersion; + private String loggerPrefix; private Builder() {} @@ -97,6 +101,18 @@ public Builder requiredSourceVersion(String requiredSourceVersion) { return this; } + /** + * Sets the diagnostic prefix every emitted log line carries ({@code "prefix: "}). Default: the + * processor's simple class name. + * + * @param loggerPrefix the logger prefix + * @return this builder + */ + public Builder loggerPrefix(String loggerPrefix) { + this.loggerPrefix = loggerPrefix; + return this; + } + /** * Builds the configuration. * @@ -108,7 +124,7 @@ public CodegenProcessorConfiguration build() { throw new IllegalStateException("elementCollector is required"); } return new CodegenProcessorConfiguration( - elementCollector, optionRegistry, requiredSourceVersion); + elementCollector, optionRegistry, requiredSourceVersion, loggerPrefix); } } } 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 6e378d0a..a4a361f0 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 @@ -128,6 +128,7 @@ public class BuilderProcessor .elementCollector(ELEMENT_COLLECTOR) .optionRegistry(OPTION_REGISTRY) .requiredSourceVersion("RELEASE_17") + .loggerPrefix("simple-builders") .build(); /** Creates the processor — javac instantiates it reflectively. */ @@ -137,12 +138,6 @@ public BuilderProcessor() { private JacksonModuleGenerator jacksonModuleGenerator; - /** The project name prefixes diagnostics — not the class name. */ - @Override - protected String provideLoggerPrefix() { - return "simple-builders"; - } - @Override protected ClassCodeGenerator createGenerator( ProcessingEnvironment processingEnv, ProcessingLogger logger, BuilderProcessingContext ctx) { From f374caad857a735b5003b0a5ca62e57c6b74e423 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:22:25 +0000 Subject: [PATCH 119/170] =?UTF-8?q?processing:=20Roaster=20is=20the=20defa?= =?UTF-8?q?ult=20code=20generator=20=E2=80=94=20domain=20overrides=20only?= =?UTF-8?q?=20for=20other=20backends?= 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 | 8 -------- 2 files changed, 7 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 2b3398d5..416cb30e 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -38,6 +38,7 @@ 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.model.core.GenerationTargetClassDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext; @@ -357,15 +358,18 @@ protected abstract C createContext( /** * Creates the code generation engine — the {@link ClassCodeGenerator} backend port the domain - * wires over {@link #generationEnvironment}. + * wires over {@link #generationEnvironment}. Default: the Roaster backend; override to plug in a + * different engine. * * @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); + protected ClassCodeGenerator createGenerator( + ProcessingEnvironment processingEnv, ProcessingLogger logger, C context) { + return new RoasterCodeGenerator(generationEnvironment(processingEnv, logger, context)); + } /** * The {@link GenerationEnvironment} the engine runs against — processing environment, logger, 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 a4a361f0..32750425 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 @@ -59,8 +59,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.ClassCodeGenerator; -import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator; 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; @@ -138,12 +136,6 @@ public BuilderProcessor() { private JacksonModuleGenerator jacksonModuleGenerator; - @Override - protected ClassCodeGenerator createGenerator( - ProcessingEnvironment processingEnv, ProcessingLogger logger, BuilderProcessingContext ctx) { - return new RoasterCodeGenerator(generationEnvironment(processingEnv, logger, ctx)); - } - @Override protected BuilderProcessingContext createContext( ProcessingEnvironment processingEnv, From f36ab8ec3d880625048d5961db830f1554db7df0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:24:08 +0000 Subject: [PATCH 120/170] =?UTF-8?q?processing:=20initGeneratorRegistry=20?= =?UTF-8?q?=E2=80=94=20the=20warmup=20is=20an=20init,=20not=20a=20get?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../simple/builders/processor/BuilderProcessor.java | 2 +- .../processor/processing/BuilderProcessingContext.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) 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 32750425..108008e3 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 @@ -152,7 +152,7 @@ protected BuilderProcessingContext createContext( // Initialize GeneratorRegistry once during processor initialization ctx.debugStartOperation("Initializing generator registry"); try { - ctx.getGeneratorRegistry(); + ctx.initGeneratorRegistry(); } finally { ctx.debugEndOperation(); } 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 2e8721b8..5e25b1f4 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 @@ -130,6 +130,11 @@ public GeneratorRegistry getGeneratorRegistry() { return generatorRegistry; } + /** Initializes the generator registry eagerly — triggers the lazy creation up front. */ + public void initGeneratorRegistry() { + getGeneratorRegistry(); + } + /** * Get the builder scope resolver for deciding whether a builder may be referenced. * From f7febbd52653ec942bee8b25b4e5e833c976b141 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:24:12 +0000 Subject: [PATCH 121/170] =?UTF-8?q?processing:=20planTasks=20locals=20name?= =?UTF-8?q?d=20after=20what=20they=20hold=20=E2=80=94=20annotatedElements/?= =?UTF-8?q?holderElements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builders/processor/BuilderProcessor.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 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 108008e3..d2953342 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 @@ -162,19 +162,19 @@ protected BuilderProcessingContext createContext( @Override protected List> planTasks( ElementGroups collected, BuilderProcessingContext ctx) { - List sortedElements = collected.byGroup(GROUP_ANNOTATED_ELEMENTS); - List sortedHolders = collected.byGroup(GROUP_EXTERNAL_TYPE_HOLDERS); - ctx.debug("Found %d annotated elements.", sortedElements.size()); - ctx.debug("Found %d type(s) for generation with @SimpleBuilderFor.", sortedHolders.size()); + List annotatedElements = collected.byGroup(GROUP_ANNOTATED_ELEMENTS); + List holderElements = collected.byGroup(GROUP_EXTERNAL_TYPE_HOLDERS); + ctx.debug("Found %d annotated elements.", annotatedElements.size()); + ctx.debug("Found %d type(s) for generation with @SimpleBuilderFor.", holderElements.size()); List> tasks = resolveGenerationPlan( - sortedElements, - sortedHolders, + annotatedElements, + holderElements, ctx.getConfigurationReader(), ctx.getPerformanceTracker()); ctx.debug( "%d of %d annotated element(s) are inside the builderGenerationPackages scope.", - tasks.size(), sortedElements.size()); + tasks.size(), annotatedElements.size()); return tasks; } From bc9af17ecbaa244c0a0b5abd374ec7b1a5d2b9af Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:24:16 +0000 Subject: [PATCH 122/170] processing: hold the annotation attribute array in a named local before iterating --- .../simple/builders/processor/BuilderProcessor.java | 5 +++-- 1 file changed, 3 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 d2953342..99e6a0a0 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 @@ -340,8 +340,9 @@ 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 : - JavaLangAnalyser.findAnnotationAttributeArray(annotationInstance, "value", context)) { + List annotationAttributeArray = + JavaLangAnalyser.findAnnotationAttributeArray(annotationInstance, "value", context); + for (Object item : annotationAttributeArray) { TypeElement target = resolveExternalTargetType(holder, item); targets.put(target.getQualifiedName().toString(), target); } From 20a882a2e2985f88f15892021fca9a73cc491b92 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:24:21 +0000 Subject: [PATCH 123/170] =?UTF-8?q?processing:=20gate=20planning=20before?= =?UTF-8?q?=20planTasks=20=E2=80=94=20no=20point=20planning=20a=20round=20?= =?UTF-8?q?that=20gets=20discarded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codegen/processing/AbstractCodegenProcessor.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 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 416cb30e..a8ca1f40 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -463,10 +463,15 @@ protected ElementGroups findElements( * @return the round's tasks, or empty when the round is gated out */ private List> plan(ElementGroups collected, C context) { - List> tasks = planTasks(collected, context); - if (collected.isEmpty() || isFinished(context)) { + if (collected.isEmpty()) { + context.debug("No matching elements collected — skipping planning for this round."); + return List.of(); + } + if (isFinished(context)) { + context.debug("Processing is already finished — skipping planning for this round."); return List.of(); } + List> tasks = planTasks(collected, context); 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. From e721eb56481cc4f1d6bc07a3c3338a1e85e35b60 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:24:35 +0000 Subject: [PATCH 124/170] =?UTF-8?q?processing:=20predictTaskResultTypeName?= =?UTF-8?q?s=20=E2=80=94=20name=20says=20it=20is=20a=20prediction;=20javad?= =?UTF-8?q?oc=20explains=20the=20forward-reference=20purpose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/AbstractCodegenProcessor.java | 23 +++++++++++-------- .../builders/processor/BuilderProcessor.java | 2 +- 2 files changed, 15 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 a8ca1f40..3bece001 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -54,7 +54,7 @@ *

A new processor declares its static surface once as {@link CodegenProcessorConfiguration} * (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 + * #planTasks}, {@link #predictTaskResultTypeNames}, {@link #analyse} and {@link * #mapToTargetSpecification}. Every other step is framework-owned or an optional {@code [default]} * the domain may override. * @@ -66,7 +66,7 @@ * 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] + * registerPlannedTaskResults(tasks, ctx) [framework; per-task names from predictTaskResultTypeNames() — implement] * loop 1 — specification (guarded, strict-mode reporting): [framework] * defs = analyse(task, ctx) [implement] * target = mapToTargetSpecification(def, cfg, ctx) (per def) [implement] @@ -494,7 +494,7 @@ private List> plan(ElementGroups 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 #plannedTaskResultTypeNames} predicts — a task may produce + * every name each task's {@link #predictTaskResultTypeNames} 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. * @@ -508,7 +508,7 @@ protected void registerPlannedTaskResults(List> tasks, C context) continue; } TypeName elementName = JavaLangMapper.mapToTypeName(targetType, context); - for (TypeName planned : plannedTaskResultTypeNames(task, context)) { + for (TypeName planned : predictTaskResultTypeNames(task, context)) { context.addGeneratedType(elementName, planned); onTaskResultRegistered(task, planned, context); } @@ -516,15 +516,20 @@ 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 #onTaskResultRegistered}. + * Predicts the names of the types this task will generate — evaluated at registration time, + * before anything is generated. The prediction lets generated types reference each other's + * planned names (see {@link AnnotationProcessingContext#resolveGeneratedType}) without a + * generation-order dependency: analysis of every task already knows every target name. + * + *

A domain whose generated types never reference other generated types may return an empty + * list — registration then simply records nothing for the task. Keep this pure: registrations and + * their side 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) + * @return the predicted generated type names (empty when the task registers nothing) */ - protected abstract List plannedTaskResultTypeNames(CodegenTask task, C context); + protected abstract List predictTaskResultTypeNames(CodegenTask task, C context); /** * Runs after a predicted type name is registered for a task — the extension point for domain 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 99e6a0a0..2ee787b6 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 @@ -433,7 +433,7 @@ private TypeName builderTypeName( * targets. */ @Override - protected List plannedTaskResultTypeNames( + protected List predictTaskResultTypeNames( CodegenTask task, BuilderProcessingContext ctx) { if (!(task.element() instanceof TypeElement targetType)) { return List.of(); From de894a7abec5811d662eed796f254b9875db1baa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:28:16 +0000 Subject: [PATCH 125/170] tests/docs: round 2 now logs the planning gate, not the empty collection --- docs/DEBUG_LOGGING.md | 4 +--- .../simple/builders/processor/BuilderProcessorTest.java | 8 +++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md index 6e5264e1..7057892e 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -132,9 +132,7 @@ When debug logging is enabled, you'll see detailed output with visual separators [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. -[INFO] [DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor. -[INFO] [DEBUG] simple-builders: 0 of 0 annotated element(s) are inside the builderGenerationPackages scope. +[INFO] [DEBUG] simple-builders: No matching elements collected — skipping planning for this round. ``` **Note**: Debug messages are prefixed with `[DEBUG]` and use `Diagnostic.Kind.OTHER` which appears as `[INFO]` in Maven output. 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 8bda3f72..4ec01856 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 @@ -164,13 +164,11 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[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 + // Round 2 — no new elements: planning is skipped before the domain hook runs "simple-builders: PROCESSING ROUND START", "[DEBUG] simple-builders: Processing round started.", - "[DEBUG] simple-builders: Found 0 annotated elements.", - "[DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor.", - "[DEBUG] simple-builders: 0 of 0 annotated element(s) are inside the" - + " builderGenerationPackages scope."); + "[DEBUG] simple-builders: No matching elements collected — skipping planning for this" + + " round."); } @Test From 70dd9bfbe102616269675d2155f6acb31a300012 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:28:16 +0000 Subject: [PATCH 126/170] =?UTF-8?q?processing:=20unset=20requiredSourceVer?= =?UTF-8?q?sion=20means=20no=20JDK=20gate=20=E2=80=94=20domains=20with=20n?= =?UTF-8?q?o=20requirement=20configure=20nothing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codegen/processing/AbstractCodegenProcessor.java | 3 +++ 1 file changed, 3 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 3bece001..f48046af 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -133,6 +133,9 @@ public synchronized void init(ProcessingEnvironment processingEnv) { * @return {@code true} when the running JDK supports the required source level */ private boolean verifySupportedJdk() { + if (requiredSourceVersion() == null) { + return true; + } SourceVersion current = processingEnv.getSourceVersion(); boolean supported = isAtLeast(current, requiredSourceVersion()); if (!supported) { From 9cd9bc90fa6c6aa5c81406a02f551c3705a48647 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:28:16 +0000 Subject: [PATCH 127/170] processing: option registry without wired options no longer NPEs on List.of --- .../codegen/processing/ProcessorOptionRegistry.java | 3 ++- 1 file changed, 2 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 34b90a57..8d2c74c8 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/ProcessorOptionRegistry.java @@ -24,6 +24,7 @@ package org.javahelpers.codegen.processing; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -88,7 +89,7 @@ public Set supportedOptions() { supported.add(option.getOptionName()); supported.add(option.getCompilerArgument()); } - for (CompilerOption wired : List.of(verboseOption, formatterProfileOption)) { + for (CompilerOption wired : Arrays.asList(verboseOption, formatterProfileOption)) { if (wired != null) { supported.add(wired.getOptionName()); supported.add(wired.getCompilerArgument()); From 1afb4acaf958f75dd75cb57d5f5d478060a834b9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:28:16 +0000 Subject: [PATCH 128/170] =?UTF-8?q?processing:=20minimal=20codegen=20proce?= =?UTF-8?q?ssor=20test=20=E2=80=94=20one=20file=20shows=20the=20domain=20s?= =?UTF-8?q?urface=20the=20driver=20asks=20for?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MinimalCodegenProcessorTest.java | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java new file mode 100644 index 00000000..a2d23485 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java @@ -0,0 +1,214 @@ +package org.javahelpers.codegen.processing; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.core.enums.FormattingMode; +import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; +import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; +import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; +import org.javahelpers.simple.builders.processor.model.method.MethodDto; +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.AnnotationProcessingContext; +import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; +import org.junit.jupiter.api.Test; + +/** + * A complete codegen processor in minimal form — only the domain members that have no generic + * default are implemented; collection, round-driving, generator and formatter wiring come from + * {@link AbstractCodegenProcessor}. + */ +public class MinimalCodegenProcessorTest { + + /** Marks a type for delegate-builder generation in this test. */ + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.SOURCE) + public @interface GenerateBuilder {} + + /** The whole per-target configuration a minimal processor needs. */ + record MinimalConfiguration() implements CodegenConfiguration { + @Override + public boolean isStrictModeEnabled() { + return false; + } + + @Override + public boolean isVerboseEnabled() { + return false; + } + } + + /** One discovered setter of the target type. */ + record Setter(String name, MethodParameterDto parameter) {} + + /** Analysis result: target type, builder name and the setters to delegate to. */ + record MinimalDefinition(TypeName sourceTypeName, TypeName builderTypeName, List setters) + implements CodegenDefinition { + @Override + public TypeName getTypeName() { + return sourceTypeName; + } + + @Override + public TypeName targetTypeName() { + return builderTypeName; + } + } + + /** A processor that generates a builder delegating to the target type's setters. */ + static class MinimalProcessor + extends AbstractCodegenProcessor< + AnnotationProcessingContext, MinimalConfiguration, MinimalDefinition> { + + MinimalProcessor() { + super( + CodegenProcessorConfiguration.builder() + .elementCollector( + ElementCollector.builder() + .selector( + selector -> + selector.groupName("targets").annotationTypes(GenerateBuilder.class)) + .build()) + .loggerPrefix("minimal") + .build()); + } + + @Override + protected AnnotationProcessingContext createContext( + ProcessingEnvironment processingEnv, + CompilerArgumentsReader reader, + ProcessingLogger logger) { + return new AnnotationProcessingContext(logger, processingEnv); + } + + @Override + protected List> planTasks( + ElementGroups collected, AnnotationProcessingContext ctx) { + return collected.byGroup("targets").stream() + .map( + element -> + new CodegenTask<>( + element, new MinimalConfiguration(), element, ctx.getPackageName(element))) + .toList(); + } + + @Override + protected List predictTaskResultTypeNames( + CodegenTask task, AnnotationProcessingContext ctx) { + return List.of(builderName((TypeElement) task.element())); + } + + @Override + protected List analyse( + CodegenTask task, AnnotationProcessingContext ctx) { + TypeElement target = (TypeElement) task.element(); + List setters = + ctx.getAllMembers(target).stream() + .filter(ExecutableElement.class::isInstance) + .map(ExecutableElement.class::cast) + .filter(method -> method.getSimpleName().toString().startsWith("set")) + .filter(method -> method.getParameters().size() == 1) + .map( + method -> + new Setter( + method.getSimpleName().toString(), + JavaLangMapper.map2MethodParameter(method.getParameters().get(0), ctx))) + .toList(); + return List.of( + new MinimalDefinition( + JavaLangMapper.mapToTypeName(target, ctx), builderName(target), setters)); + } + + @Override + protected GenerationTargetClassDto mapToTargetSpecification( + MinimalDefinition definition, + MinimalConfiguration configuration, + AnnotationProcessingContext ctx) { + GenerationTargetClassDto target = new GenerationTargetClassDto(); + target.setTypeName(definition.builderTypeName()); + target.setClassAccessModifier(AccessModifier.PUBLIC); + target.setFormattingMode(FormattingMode.NONE); + + ClassFieldDto field = new ClassFieldDto(); + field.setFieldName("target"); + field.setFieldType(definition.sourceTypeName()); + field.setVisibility(AccessModifier.PRIVATE); + field.setLiteralInitializer("new " + definition.sourceTypeName().getClassName() + "()"); + target.addClassField(field); + + for (Setter setter : definition.setters()) { + String name = setter.name().substring(3, 4).toLowerCase() + setter.name().substring(4); + MethodDto method = new MethodDto(name, definition.builderTypeName()); + method.setModifier(AccessModifier.PUBLIC); + method.addParameter(setter.parameter()); + method.setCode( + "target." + + setter.name() + + "(" + + setter.parameter().getParameterName() + + ");\nreturn this;"); + target.addMethod(method); + } + MethodDto build = new MethodDto("build", definition.sourceTypeName()); + build.setModifier(AccessModifier.PUBLIC); + build.setCode("return target;"); + target.addMethod(build); + return target; + } + + private static TypeName builderName(TypeElement target) { + String qualified = target.getQualifiedName().toString(); + return new TypeName( + qualified.substring(0, qualified.lastIndexOf('.')), target.getSimpleName() + "Builder"); + } + } + + @Test + void minimalProcessorGeneratesDelegatingBuilder() throws IOException { + JavaFileObject source = + JavaFileObjects.forSourceString( + "test.Person", + """ + package test; + import org.javahelpers.codegen.processing.MinimalCodegenProcessorTest.GenerateBuilder; + @GenerateBuilder + public class Person { + private String name; + public void setName(String name) { this.name = name; } + } + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler(new MinimalProcessor()).compile(source); + + assertTrue( + compilation.status() == Compilation.Status.SUCCESS, compilation.diagnostics().toString()); + JavaFileObject generated = + compilation + .generatedSourceFile("test.PersonBuilder") + .orElseThrow(() -> new AssertionError("no PersonBuilder generated")); + String code = generated.getCharContent(true).toString(); + assertTrue(code.contains("public class PersonBuilder"), code); + assertTrue(code.contains("target.setName(name);"), code); + assertTrue(code.contains("public Person build()"), code); + assertTrue( + compilation.diagnostics().stream().noneMatch(d -> d.getKind() == Diagnostic.Kind.ERROR), + compilation.diagnostics().toString()); + } +} From d42ce12e3b6a0ab8e0821462cf92b9dd5b2ec9b1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:17:05 +0000 Subject: [PATCH 129/170] =?UTF-8?q?processing:=20getGeneratorRegistry=20re?= =?UTF-8?q?named=20initGeneratorRegistry=20=E2=80=94=20the=20lazy=20getter?= =?UTF-8?q?=20IS=20the=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/BuilderDefinitionCreator.java | 4 ++-- .../processing/BuilderProcessingContext.java | 14 ++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index b33e4ac5..e88f114e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -129,7 +129,7 @@ public static BuilderDefinitionDto extractFromElement( result.addAllFields(setterFields); // Apply builder enhancers (including With interface generation) - context.getGeneratorRegistry().enhanceBuilder(result, result.getBuildingTargetTypeName()); + context.initGeneratorRegistry().enhanceBuilder(result, result.getBuildingTargetTypeName()); // Propagate @Deprecated from the DTO type to the generated builder class and its factory // methods. Done after enhancement so the class javadoc, create() method and constructors @@ -878,7 +878,7 @@ private static Optional createFieldDto( // Use GeneratorRegistry to generate all methods for this field List generatedMethods = - context.getGeneratorRegistry().generateAllMethods(field, dtoType, builderType); + context.initGeneratorRegistry().generateAllMethods(field, dtoType, builderType); generatedMethods.forEach(field::addMethod); // Apply @Deprecated annotation and @deprecated javadoc to every generated builder method for 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 5e25b1f4..65b88aae 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 @@ -116,25 +116,19 @@ public BuilderConfigurationReader getConfigurationReader() { } /** - * Get the unified generator registry for both field-level method generation and builder-level - * enhancement. - * - *

The registry is lazily initialized on first access to avoid circular dependency issues. + * The unified generator registry for both field-level method generation and builder-level + * enhancement — lazily initialized on first access to avoid circular dependency issues, so the + * first call doubles as the init. * * @return the generator registry */ - public GeneratorRegistry getGeneratorRegistry() { + public GeneratorRegistry initGeneratorRegistry() { if (generatorRegistry == null) { generatorRegistry = new GeneratorRegistry(this, processingEnv); } return generatorRegistry; } - /** Initializes the generator registry eagerly — triggers the lazy creation up front. */ - public void initGeneratorRegistry() { - getGeneratorRegistry(); - } - /** * Get the builder scope resolver for deciding whether a builder may be referenced. * From 9f296bf76e3575dfd09129d1598c347a692d59f8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:17:09 +0000 Subject: [PATCH 130/170] =?UTF-8?q?processing:=20ElementGroups.all()=20?= =?UTF-8?q?=E2=80=94=20collect=20without=20caring=20about=20group=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../javahelpers/codegen/processing/ElementGroups.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 91b990c0..30564afb 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementGroups.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementGroups.java @@ -50,6 +50,15 @@ public List byGroup(String groupName) { return groups.getOrDefault(groupName, List.of()); } + /** + * Every selected element across all groups, in group declaration order. + * + * @return all selected elements + */ + public List all() { + return groups.values().stream().flatMap(List::stream).toList(); + } + /** * Whether every selected group is empty. * From 67ac07dfcfe787fb4cddd01cd3acb97aa85bb495 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:17:19 +0000 Subject: [PATCH 131/170] =?UTF-8?q?processing:=20selector(annotationTypes?= =?UTF-8?q?=E2=80=A6)=20convenience=20on=20the=20collector=20builder=20?= =?UTF-8?q?=E2=80=94=20the=20single-annotation=20case=20without=20group=20?= =?UTF-8?q?plumbing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codegen/processing/ElementCollector.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 cf95ce77..e1f570d4 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementCollector.java @@ -86,6 +86,20 @@ public Builder selector(Consumer selectorDeclaration) { return selector(selectorBuilder.build()); } + /** + * Adds a selector matching elements by annotation type alone — the convenience for the common + * single-annotation case with no meta-marker, opt-out or named group (read the result via + * {@link ElementGroups#all()}). + * + * @param annotationTypes the annotation types selecting an element + * @return this builder + */ + @SafeVarargs + public final Builder selector( + Class... annotationTypes) { + return selector(selector -> selector.groupName("default").annotationTypes(annotationTypes)); + } + /** Builds the spec. */ public ElementCollector build() { return new ElementCollector(selectors); From 75d26817b6132d349d80b8063d847a1ae5b441a8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:17:24 +0000 Subject: [PATCH 132/170] =?UTF-8?q?processing:=20CodegenConfiguration.DEFA?= =?UTF-8?q?ULT=20=E2=80=94=20the=20framework=20ships=20the=20plain=20stric?= =?UTF-8?q?t/non-verbose=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codegen/processing/CodegenConfiguration.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java index 8894fa4b..4607fcb7 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenConfiguration.java @@ -34,6 +34,20 @@ */ public interface CodegenConfiguration { + /** The framework default: strict mode enabled, verbose disabled. */ + CodegenConfiguration DEFAULT = + new CodegenConfiguration() { + @Override + public boolean isStrictModeEnabled() { + return true; + } + + @Override + public boolean isVerboseEnabled() { + return false; + } + }; + /** * Whether generation problems are reported as errors (fail-fast) instead of warnings. * From 461319cbe57403d03c8bfe04dda8ccd7631cdced Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:17:34 +0000 Subject: [PATCH 133/170] =?UTF-8?q?processing:=20CodegenTask.builder()=20?= =?UTF-8?q?=E2=80=94=20reportingElement=20defaults=20to=20the=20task=20ele?= =?UTF-8?q?ment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codegen/processing/CodegenTask.java | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) 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 481895f6..7939bd70 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java @@ -44,4 +44,56 @@ * before analysis */ public record CodegenTask( - Element element, K configuration, Element reportingElement, String outputPackage) {} + Element element, K configuration, Element reportingElement, String outputPackage) { + + /** + * A task builder — {@code reportingElement} defaults to the task's element when not set. + * + * @param the configuration type + * @return a new builder + */ + public static Builder builder() { + return new Builder<>(); + } + + /** Fluent builder for {@link CodegenTask}. */ + public static final class Builder { + private Element element; + private K configuration; + private Element reportingElement; + private String outputPackage; + + /** The element the task generates for. */ + public Builder element(Element element) { + this.element = element; + return this; + } + + /** The resolved per-element configuration. */ + public Builder configuration(K configuration) { + this.configuration = configuration; + return this; + } + + /** The element diagnostics are reported on; defaults to {@link #element}. */ + public Builder reportingElement(Element reportingElement) { + this.reportingElement = reportingElement; + return this; + } + + /** The package generated code is written to, or {@code null}. */ + public Builder outputPackage(String outputPackage) { + this.outputPackage = outputPackage; + return this; + } + + /** Builds the task; the reporting element defaults to the task element. */ + public CodegenTask build() { + return new CodegenTask<>( + element, + configuration, + reportingElement != null ? reportingElement : element, + outputPackage); + } + } +} From fc6b7ed99ac006214cf13cbbf372cf47a5ce2c88 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:18:07 +0000 Subject: [PATCH 134/170] =?UTF-8?q?test:=20minimal=20processor=20reworked?= =?UTF-8?q?=20on=20the=20framework=20conveniences=20=E2=80=94=20selector(C?= =?UTF-8?q?lass),=20all(),=20task=20builder,=20DEFAULT=20config,=20setter?= =?UTF-8?q?=20finder,=20placeholders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MinimalCodegenProcessorTest.java | 127 +++++++----------- 1 file changed, 50 insertions(+), 77 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java index a2d23485..472bb979 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java @@ -1,10 +1,10 @@ package org.javahelpers.codegen.processing; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static com.google.testing.compile.CompilationSubject.assertThat; +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.contains; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; import com.google.testing.compile.Compilation; -import com.google.testing.compile.JavaFileObjects; -import java.io.IOException; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -13,19 +13,20 @@ import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; -import javax.tools.Diagnostic; import javax.tools.JavaFileObject; +import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.enums.FormattingMode; +import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; -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.AnnotationProcessingContext; import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; +import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; import org.junit.jupiter.api.Test; @@ -41,24 +42,9 @@ public class MinimalCodegenProcessorTest { @Retention(RetentionPolicy.SOURCE) public @interface GenerateBuilder {} - /** The whole per-target configuration a minimal processor needs. */ - record MinimalConfiguration() implements CodegenConfiguration { - @Override - public boolean isStrictModeEnabled() { - return false; - } - - @Override - public boolean isVerboseEnabled() { - return false; - } - } - - /** One discovered setter of the target type. */ - record Setter(String name, MethodParameterDto parameter) {} - /** Analysis result: target type, builder name and the setters to delegate to. */ - record MinimalDefinition(TypeName sourceTypeName, TypeName builderTypeName, List setters) + record MinimalTargetDefinition( + TypeName sourceTypeName, TypeName builderTypeName, List setters) implements CodegenDefinition { @Override public TypeName getTypeName() { @@ -74,17 +60,12 @@ public TypeName targetTypeName() { /** A processor that generates a builder delegating to the target type's setters. */ static class MinimalProcessor extends AbstractCodegenProcessor< - AnnotationProcessingContext, MinimalConfiguration, MinimalDefinition> { + AnnotationProcessingContext, CodegenConfiguration, MinimalTargetDefinition> { MinimalProcessor() { super( CodegenProcessorConfiguration.builder() - .elementCollector( - ElementCollector.builder() - .selector( - selector -> - selector.groupName("targets").annotationTypes(GenerateBuilder.class)) - .build()) + .elementCollector(ElementCollector.builder().selector(GenerateBuilder.class).build()) .loggerPrefix("minimal") .build()); } @@ -98,47 +79,40 @@ protected AnnotationProcessingContext createContext( } @Override - protected List> planTasks( + protected List> planTasks( ElementGroups collected, AnnotationProcessingContext ctx) { - return collected.byGroup("targets").stream() + return collected.all().stream() .map( element -> - new CodegenTask<>( - element, new MinimalConfiguration(), element, ctx.getPackageName(element))) + CodegenTask.builder() + .element(element) + .configuration(CodegenConfiguration.DEFAULT) + .outputPackage(ctx.getPackageName(element)) + .build()) .toList(); } @Override protected List predictTaskResultTypeNames( - CodegenTask task, AnnotationProcessingContext ctx) { + CodegenTask task, AnnotationProcessingContext ctx) { return List.of(builderName((TypeElement) task.element())); } @Override - protected List analyse( - CodegenTask task, AnnotationProcessingContext ctx) { + protected List analyse( + CodegenTask task, AnnotationProcessingContext ctx) { TypeElement target = (TypeElement) task.element(); - List setters = - ctx.getAllMembers(target).stream() - .filter(ExecutableElement.class::isInstance) - .map(ExecutableElement.class::cast) - .filter(method -> method.getSimpleName().toString().startsWith("set")) - .filter(method -> method.getParameters().size() == 1) - .map( - method -> - new Setter( - method.getSimpleName().toString(), - JavaLangMapper.map2MethodParameter(method.getParameters().get(0), ctx))) - .toList(); return List.of( - new MinimalDefinition( - JavaLangMapper.mapToTypeName(target, ctx), builderName(target), setters)); + new MinimalTargetDefinition( + JavaLangMapper.mapToTypeName(target, ctx), + builderName(target), + JavaLangAnalyser.findAllPossibleSettersOfClass(target, ctx))); } @Override protected GenerationTargetClassDto mapToTargetSpecification( - MinimalDefinition definition, - MinimalConfiguration configuration, + MinimalTargetDefinition definition, + CodegenConfiguration configuration, AnnotationProcessingContext ctx) { GenerationTargetClassDto target = new GenerationTargetClassDto(); target.setTypeName(definition.builderTypeName()); @@ -152,17 +126,19 @@ protected GenerationTargetClassDto mapToTargetSpecification( field.setLiteralInitializer("new " + definition.sourceTypeName().getClassName() + "()"); target.addClassField(field); - for (Setter setter : definition.setters()) { - String name = setter.name().substring(3, 4).toLowerCase() + setter.name().substring(4); - MethodDto method = new MethodDto(name, definition.builderTypeName()); + for (ExecutableElement setter : definition.setters()) { + String setterName = setter.getSimpleName().toString(); + String methodName = StringUtils.uncapitalize(StringUtils.removeStart(setterName, "set")); + MethodDto method = new MethodDto(methodName, definition.builderTypeName()); method.setModifier(AccessModifier.PUBLIC); - method.addParameter(setter.parameter()); + String paramName = setter.getParameters().get(0).getSimpleName().toString(); + method.addParameter(JavaLangMapper.map2MethodParameter(setter.getParameters().get(0), ctx)); method.setCode( - "target." - + setter.name() - + "(" - + setter.parameter().getParameterName() - + ");\nreturn this;"); + """ + target.$setterName:N($paramName:N); + return this;"""); + method.addArgument("setterName", setterName); + method.addArgument("paramName", paramName); target.addMethod(method); } MethodDto build = new MethodDto("build", definition.sourceTypeName()); @@ -180,10 +156,10 @@ private static TypeName builderName(TypeElement target) { } @Test - void minimalProcessorGeneratesDelegatingBuilder() throws IOException { + void minimalProcessorGeneratesDelegatingBuilder() { + // Given JavaFileObject source = - JavaFileObjects.forSourceString( - "test.Person", + ProcessorTestUtils.forSource( """ package test; import org.javahelpers.codegen.processing.MinimalCodegenProcessorTest.GenerateBuilder; @@ -194,21 +170,18 @@ public class Person { } """); + // When Compilation compilation = ProcessorTestUtils.createCompiler(new MinimalProcessor()).compile(source); - assertTrue( - compilation.status() == Compilation.Status.SUCCESS, compilation.diagnostics().toString()); - JavaFileObject generated = - compilation - .generatedSourceFile("test.PersonBuilder") - .orElseThrow(() -> new AssertionError("no PersonBuilder generated")); - String code = generated.getCharContent(true).toString(); - assertTrue(code.contains("public class PersonBuilder"), code); - assertTrue(code.contains("target.setName(name);"), code); - assertTrue(code.contains("public Person build()"), code); - assertTrue( - compilation.diagnostics().stream().noneMatch(d -> d.getKind() == Diagnostic.Kind.ERROR), - compilation.diagnostics().toString()); + // Then + assertThat(compilation).succeeded(); + String generatedCode = loadGeneratedSource(compilation, "PersonBuilder"); + ProcessorAsserts.assertingResult( + generatedCode, + contains("public class PersonBuilder"), + contains("private Person target = new Person();"), + contains("target.setName(name);"), + contains("public Person build()")); } } From 1a4a9bb498ad06b8014b66b21890ab135d58d8e1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:22:39 +0000 Subject: [PATCH 135/170] =?UTF-8?q?test:=20guard=20non-type=20task=20eleme?= =?UTF-8?q?nts=20instead=20of=20casting=20=E2=80=94=20a=20domain=20plans?= =?UTF-8?q?=20only=20what=20its=20selector=20collects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codegen/processing/MinimalCodegenProcessorTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java index 472bb979..6e90a6e1 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java @@ -95,13 +95,18 @@ protected List> planTasks( @Override protected List predictTaskResultTypeNames( CodegenTask task, AnnotationProcessingContext ctx) { - return List.of(builderName((TypeElement) task.element())); + if (!(task.element() instanceof TypeElement target)) { + return List.of(); + } + return List.of(builderName(target)); } @Override protected List analyse( CodegenTask task, AnnotationProcessingContext ctx) { - TypeElement target = (TypeElement) task.element(); + if (!(task.element() instanceof TypeElement target)) { + return List.of(); + } return List.of( new MinimalTargetDefinition( JavaLangMapper.mapToTypeName(target, ctx), From 333f18b1965edab7329151155303a0038808966a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:25:51 +0000 Subject: [PATCH 136/170] test: pin the whole generated PersonBuilder as one expected textblock --- .../MinimalCodegenProcessorTest.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java index 6e90a6e1..b3100760 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java @@ -1,8 +1,8 @@ package org.javahelpers.codegen.processing; import static com.google.testing.compile.CompilationSubject.assertThat; -import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.contains; import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; +import static org.junit.jupiter.api.Assertions.assertEquals; import com.google.testing.compile.Compilation; import java.lang.annotation.ElementType; @@ -26,7 +26,6 @@ import org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext; import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; -import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; import org.junit.jupiter.api.Test; @@ -182,11 +181,23 @@ public class Person { // Then assertThat(compilation).succeeded(); String generatedCode = loadGeneratedSource(compilation, "PersonBuilder"); - ProcessorAsserts.assertingResult( - generatedCode, - contains("public class PersonBuilder"), - contains("private Person target = new Person();"), - contains("target.setName(name);"), - contains("public Person build()")); + // The whole expected target as one textblock — the Roaster output pins structure, + // delegation and the target field; nothing generated is left unchecked. + assertEquals( + """ + package test; + public class PersonBuilder { + + \tprivate Person target = new Person(); + + \tpublic PersonBuilder name(String name) { + \t\ttarget.setName(name); + \t\treturn this; + \t} + + \tpublic Person build() { + \t\treturn target; + \t} }""", + generatedCode.strip()); } } From 22391b71ea161940061bac669de4ee53e90c1629 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:33:31 +0000 Subject: [PATCH 137/170] test: reuse ctx.getPackageName for builder package extraction --- .../processing/MinimalCodegenProcessorTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java index b3100760..d63652ce 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java @@ -97,7 +97,7 @@ protected List predictTaskResultTypeNames( if (!(task.element() instanceof TypeElement target)) { return List.of(); } - return List.of(builderName(target)); + return List.of(builderName(target, ctx)); } @Override @@ -109,7 +109,7 @@ protected List analyse( return List.of( new MinimalTargetDefinition( JavaLangMapper.mapToTypeName(target, ctx), - builderName(target), + builderName(target, ctx), JavaLangAnalyser.findAllPossibleSettersOfClass(target, ctx))); } @@ -152,10 +152,8 @@ protected GenerationTargetClassDto mapToTargetSpecification( return target; } - private static TypeName builderName(TypeElement target) { - String qualified = target.getQualifiedName().toString(); - return new TypeName( - qualified.substring(0, qualified.lastIndexOf('.')), target.getSimpleName() + "Builder"); + private static TypeName builderName(TypeElement target, AnnotationProcessingContext ctx) { + return new TypeName(ctx.getPackageName(target), target.getSimpleName() + "Builder"); } } From 5918ac7a33138f95b005227bf117cdd392647a5c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:33:40 +0000 Subject: [PATCH 138/170] test: use Strings.CI.removeStart for setter prefix like elsewhere --- .../codegen/processing/MinimalCodegenProcessorTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java index d63652ce..969857fe 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java @@ -15,6 +15,7 @@ import javax.lang.model.element.TypeElement; import javax.tools.JavaFileObject; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.enums.FormattingMode; import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser; @@ -132,7 +133,7 @@ protected GenerationTargetClassDto mapToTargetSpecification( for (ExecutableElement setter : definition.setters()) { String setterName = setter.getSimpleName().toString(); - String methodName = StringUtils.uncapitalize(StringUtils.removeStart(setterName, "set")); + String methodName = StringUtils.uncapitalize(Strings.CI.removeStart(setterName, "set")); MethodDto method = new MethodDto(methodName, definition.builderTypeName()); method.setModifier(AccessModifier.PUBLIC); String paramName = setter.getParameters().get(0).getSimpleName().toString(); From 8eb1f2ddcefbd7c6c493beed94c778898e16eee1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:34:23 +0000 Subject: [PATCH 139/170] test: assertThrows with single-call lambda (Sonar S5778) --- .../codegen/processing/CodegenProcessorConfigurationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java index 973b1823..ccff53be 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/CodegenProcessorConfigurationTest.java @@ -34,6 +34,6 @@ class CodegenProcessorConfigurationTest { @Test void build_withoutElementCollector_throwsIllegalState() { CodegenProcessorConfiguration.Builder builder = CodegenProcessorConfiguration.builder(); - assertThrows(IllegalStateException.class, builder::build); + assertThrows(IllegalStateException.class, () -> builder.build()); } } From 57b5170d23c21ddad5f74798df88ad9288d688dc Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:35:11 +0000 Subject: [PATCH 140/170] test: add missing license header to MinimalCodegenProcessorTest --- .../MinimalCodegenProcessorTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java index 969857fe..7317fbf9 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java @@ -1,3 +1,26 @@ +/* + * 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 static com.google.testing.compile.CompilationSubject.assertThat; From a54c992e7240553145671fe36f268e8918910c09 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:39:49 +0000 Subject: [PATCH 141/170] test: literal tabs in expected textblock, closing delimiter at column 0 --- .../MinimalCodegenProcessorTest.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java index 7317fbf9..89bf143f 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java @@ -206,20 +206,20 @@ public class Person { // The whole expected target as one textblock — the Roaster output pins structure, // delegation and the target field; nothing generated is left unchecked. assertEquals( - """ - package test; - public class PersonBuilder { +""" +package test; +public class PersonBuilder { - \tprivate Person target = new Person(); + private Person target = new Person(); - \tpublic PersonBuilder name(String name) { - \t\ttarget.setName(name); - \t\treturn this; - \t} + public PersonBuilder name(String name) { + target.setName(name); + return this; + } - \tpublic Person build() { - \t\treturn target; - \t} }""", + public Person build() { + return target; + } }""", generatedCode.strip()); } } From 84789b6af0ff92482d47eaf478eb8fa7ccb52cde Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:46:30 +0000 Subject: [PATCH 142/170] test: full compare via assertNormalizedEquals, normal indentation --- .../MinimalCodegenProcessorTest.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java index 89bf143f..77076974 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java @@ -24,8 +24,8 @@ package org.javahelpers.codegen.processing; import static com.google.testing.compile.CompilationSubject.assertThat; +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertNormalizedEquals; import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; -import static org.junit.jupiter.api.Assertions.assertEquals; import com.google.testing.compile.Compilation; import java.lang.annotation.ElementType; @@ -203,23 +203,25 @@ public class Person { // Then assertThat(compilation).succeeded(); String generatedCode = loadGeneratedSource(compilation, "PersonBuilder"); - // The whole expected target as one textblock — the Roaster output pins structure, - // delegation and the target field; nothing generated is left unchecked. - assertEquals( -""" -package test; -public class PersonBuilder { - - private Person target = new Person(); - - public PersonBuilder name(String name) { - target.setName(name); - return this; - } - - public Person build() { - return target; - } }""", - generatedCode.strip()); + // The whole expected target as one textblock — pins structure, delegation and the target + // field; whitespace-normalized compare, nothing generated is left unchecked. + assertNormalizedEquals( + """ + package test; + public class PersonBuilder { + + private Person target = new Person(); + + public PersonBuilder name(String name) { + target.setName(name); + return this; + } + + public Person build() { + return target; + } + }""", + generatedCode, + "generated PersonBuilder must match the expected source"); } } From 4e190748f6684f8e86d34610fcb0325ee7627536 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:54:21 +0000 Subject: [PATCH 143/170] test: de-flake classMetrics ordering asserts (timing-dependent positions) --- .../logging/ActivePerformanceTrackerTest.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) 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 73bb3391..80730209 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.Set; import org.javahelpers.simple.builders.processor.classgen.GenerationPhases; import org.javahelpers.simple.builders.processor.processing.PipelinePhases; import org.javahelpers.simple.builders.processor.testing.CapturingProcessingLogger; @@ -308,8 +309,12 @@ void jsonReport_classMetrics_containsAllClassesSortedByElapsedDesc() throws IOEx JsonNode classMetrics = root.get("classMetrics"); assertEquals(2, classMetrics.size()); - assertEquals("TestClassA", classMetrics.get(0).get("className").asText()); - assertEquals("TestClassB", classMetrics.get(1).get("className").asText()); + // Ordering is the contract (desc by elapsed); which class is first is timing-dependent. + assertEquals( + Set.of("TestClassA", "TestClassB"), + Set.of( + classMetrics.get(0).get("className").asText(), + classMetrics.get(1).get("className").asText())); assertTrue( classMetrics.get(0).get("elapsedNanos").asLong() >= classMetrics.get(1).get("elapsedNanos").asLong()); @@ -320,7 +325,13 @@ void jsonReport_classMetrics_containsFieldAndCollectionCounts() throws IOExcepti Path jsonFile = tempDir.resolve("report.json"); JsonNode root = generateReportAndParseJson(jsonFile.toString()); - JsonNode classA = root.get("classMetrics").get(0); + JsonNode classA = null; + for (JsonNode cm : root.get("classMetrics")) { + if ("TestClassA".equals(cm.get("className").asText())) { + classA = cm; + } + } + assertNotNull(classA); assertEquals(5, classA.get("fieldCount").asInt()); assertEquals(2, classA.get("collectionCount").asInt()); assertTrue(classA.has("elapsedMs")); From b89d5ac70cd666f4d7837809d15075c550f3259d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:19:06 +0000 Subject: [PATCH 144/170] Update DEBUG_LOGGING example to real emitted messages Replace stale texts (Code generation for builder, Class builder created, Writing builder class) with the actual sequence: config resolution, enhancer pass, finalize, codegen phases, Jackson tail. Fix chanel typo. --- docs/DEBUG_LOGGING.md | 80 +++++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md index 7057892e..5f6d747f 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -76,7 +76,7 @@ The Simple Builders processor provides **conditional hierarchical logging** to b - Example: `simple-builders: Successfully generated 3 target(s) in this processing round` ### **When Debug Mode is ENABLED (-Averbose=true):** -- Every message carries the `simple-builders:` logger prefix; DEBUG messages are posted on the `[INFO]` chanel so they show as `[INFO] [DEBUG]` +- Every message carries the `simple-builders:` logger prefix; DEBUG messages are posted on the `[INFO]` channel so they show as `[INFO] [DEBUG]` - Provides full visibility into the processing hierarchy - Example: `[INFO] simple-builders: │ └─ Successfully generated 3 target(s) in this processing round` - Example: `[WARNING] simple-builders: │ │ ├─ Builder field conflict: field 'name'...` @@ -90,49 +90,91 @@ This approach ensures clean production logs while maintaining full debugging cap When debug logging is enabled, you'll see detailed output with visual separators: ``` +[INFO] [DEBUG] simple-builders: Starting BuilderProcessor... +[INFO] [DEBUG] simple-builders: Loaded global configuration from compiler arguments: BuilderConfiguration[verbose=ENABLED] +[INFO] [DEBUG] simple-builders: Initializing generator registry +[INFO] [DEBUG] simple-builders: ├─ Loaded 16 method generators and 10 builder enhancers total +[INFO] [DEBUG] simple-builders: └─ Initialized GeneratorRegistry with 16 method generators and 10 builder enhancers [INFO] simple-builders: PROCESSING ROUND START [INFO] [DEBUG] simple-builders: Processing round started. [INFO] [DEBUG] simple-builders: Found 3 annotated elements. [INFO] [DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor. +[INFO] [DEBUG] simple-builders: Processing element: PersonDto +[INFO] [DEBUG] simple-builders: ├─ Resolving configuration for element: PersonDto +[INFO] [DEBUG] simple-builders: │ ├─ Built-in template @SimpleBuilder found in DIRECT scope +[INFO] [DEBUG] simple-builders: │ └─ Resulting configuration resolved: BuilderConfiguration[builderSuffix=Builder,...] +[INFO] [DEBUG] simple-builders: Processing element: OrderDto +[INFO] [DEBUG] simple-builders: ├─ Resolving configuration for element: OrderDto +[INFO] [DEBUG] simple-builders: │ └─ Resulting configuration resolved: BuilderConfiguration[builderSuffix=Builder,...] +[INFO] [DEBUG] simple-builders: Processing element: CustomerDto +[INFO] [DEBUG] simple-builders: ├─ Resolving configuration for element: CustomerDto +[INFO] [DEBUG] simple-builders: │ └─ Resulting configuration resolved: BuilderConfiguration[builderSuffix=Builder,...] [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: │ │ │ ├─ Processing method generators +[INFO] [DEBUG] simple-builders: │ │ │ │ ├─ Applying: BasicSetterGenerator (priority: 100) +[INFO] [DEBUG] simple-builders: │ │ │ │ └─ Generated 1 methods [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: │ │ └─ Processed 2 possible setters: added 2 fields, skipped 0 +[INFO] [DEBUG] simple-builders: │ ├─ Processing class based enhancer +[INFO] [DEBUG] simple-builders: │ │ ├─ Applying: CoreMethodsEnhancer (priority: 100) +[INFO] [DEBUG] simple-builders: │ │ ├─ Applying: ConstructorEnhancer (priority: 95) +[INFO] [DEBUG] simple-builders: │ │ ├─ Applying: ClassJavaDocEnhancer (priority: 10) +[INFO] [DEBUG] simple-builders: │ │ └─ Applied 3 builder enhancers +[INFO] [DEBUG] simple-builders: │ ├─ Finalizing builder definition +[INFO] [DEBUG] simple-builders: │ │ ├─ Resolving method conflicts +[INFO] [DEBUG] simple-builders: │ │ │ └─ Resolved: 7 signatures, 0 conflicts, 0 methods removed +[INFO] [DEBUG] simple-builders: │ │ └─ Finalized: 2 class fields, 5 builder-level methods, 2 constructors +[INFO] [DEBUG] simple-builders: │ ├─ Builder will be generated as: PersonDtoBuilder +[INFO] [DEBUG] simple-builders: │ └─ Builder definition extracted: PersonDtoBuilder [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: │ └─ Builder definition extracted: OrderDtoBuilder +[INFO] [DEBUG] simple-builders: Processing element: CustomerDto [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: │ └─ Builder definition extracted: CustomerDtoBuilder [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: ├─ Code generation for class: PersonDtoBuilder +[INFO] [DEBUG] simple-builders: │ ├─ JavaClassSource created +[INFO] [DEBUG] simple-builders: │ ├─ Class metadata added +[INFO] [DEBUG] simple-builders: │ ├─ Generating 2 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: │ ├─ Generating 2 constructors +[INFO] [DEBUG] simple-builders: │ │ └─ Constructors added: 2 +[INFO] [DEBUG] simple-builders: │ ├─ Generating 7 method candidates +[INFO] [DEBUG] simple-builders: │ │ ├─ Resolved to 7 methods after conflict resolution +[INFO] [DEBUG] simple-builders: │ │ └─ Methods added: 7 +[INFO] [DEBUG] simple-builders: │ ├─ Generating 1 nested type(s) +[INFO] [DEBUG] simple-builders: │ │ ├─ Generated nested type: With +[INFO] [DEBUG] simple-builders: │ │ └─ Nested types added +[INFO] [DEBUG] simple-builders: │ ├─ Class-level annotations 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: │ ├─ Writing class to file: org.example.PersonDtoBuilder +[INFO] [DEBUG] simple-builders: │ └─ Successfully generated class: PersonDtoBuilder +[INFO] [DEBUG] simple-builders: ├─ Jackson module entry added +[INFO] [DEBUG] simple-builders: └─ Generated builder with 2 fields and 7 methods for 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 class: OrderDtoBuilder +[INFO] [DEBUG] simple-builders: │ └─ Successfully generated class: 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 class: CustomerDtoBuilder +[INFO] [DEBUG] simple-builders: │ └─ Successfully generated class: 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. -[INFO] [DEBUG] simple-builders: No matching elements collected — skipping planning for this round. +[INFO] [DEBUG] simple-builders: Found 0 annotated elements. +[INFO] [DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor. +[INFO] [DEBUG] simple-builders: 0 of 0 annotated element(s) are inside the builderGenerationPackages scope. +[INFO] simple-builders: Processing OVER. JacksonModuleEnabled: true, Packages: 1 +[INFO] simple-builders: Generating Jackson module in package 'org.example' +[INFO] [DEBUG] simple-builders: Code generation for class: SimpleBuildersJacksonModule ``` **Note**: Debug messages are prefixed with `[DEBUG]` and use `Diagnostic.Kind.OTHER` which appears as `[INFO]` in Maven output. From 3b450632a727296353339a696031e80635ff9986 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:19:09 +0000 Subject: [PATCH 145/170] Update CONTRIBUTING verbose log excerpt to real emitted messages Same stale texts as DEBUG_LOGGING: config resolution pass, enhancer pass, finalize, codegen phases and per-builder summary added; old 'Adding Methods'/'generated builder' lines removed. --- docs/CONTRIBUTING.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index ceec7365..b8c015c9 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -242,6 +242,9 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md). [DEBUG] simple-builders: Processing round started. [DEBUG] simple-builders: Found 1 annotated elements. [DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor. +[DEBUG] simple-builders: Processing element: Project +[DEBUG] simple-builders: ├─ Resolving configuration for element: Project +[DEBUG] simple-builders: │ └─ Resulting configuration resolved: BuilderConfiguration[builderSuffix=Builder,...] [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 @@ -249,13 +252,24 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md). [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: │ │ └─ Processed 1 possible setters: added 1 fields, skipped 0 +[DEBUG] simple-builders: │ ├─ Processing class based enhancer +[DEBUG] simple-builders: │ │ └─ Applied 8 builder enhancers +[DEBUG] simple-builders: │ ├─ Finalizing builder definition +[DEBUG] simple-builders: │ │ └─ Finalized: 1 class fields, 4 builder-level methods, 2 constructors +[DEBUG] simple-builders: │ └─ Builder definition extracted: ProjectBuilder [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 +[DEBUG] simple-builders: ├─ Code generation for class: ProjectBuilder +[DEBUG] simple-builders: │ ├─ Generating 1 fields +[DEBUG] simple-builders: │ │ └─ Fields added: 1 fields +[DEBUG] simple-builders: │ ├─ Generating 4 method candidates +[DEBUG] simple-builders: │ │ ├─ Resolved to 4 methods after conflict resolution +[DEBUG] simple-builders: │ │ └─ Methods added: 4 +[DEBUG] simple-builders: │ ├─ Writing class to file: test.ProjectBuilder +[DEBUG] simple-builders: │ └─ Successfully generated class: ProjectBuilder +[DEBUG] simple-builders: ├─ Jackson module entry added +[DEBUG] simple-builders: └─ Generated builder with 1 fields and 4 methods for ProjectBuilder + simple-builders: Successfully generated 1 target(s) in this processing round ============================================= ========== Generated Source Files ========== From a9d4dfd7c0ccff896cbab1b9f77690bf7dfb9c10 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:21:15 +0000 Subject: [PATCH 146/170] Make predictTaskResultTypeNames a default-empty hook Empty was already documented as legitimate; domains without cross-generated-type references should not implement it. MinimalProcessor drops its override. --- .../processing/AbstractCodegenProcessor.java | 17 +++++++++-------- .../processing/MinimalCodegenProcessorTest.java | 9 --------- 2 files changed, 9 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 f48046af..fae474fc 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -54,9 +54,8 @@ *

A new processor declares its static surface once as {@link CodegenProcessorConfiguration} * (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 #predictTaskResultTypeNames}, {@link #analyse} and {@link - * #mapToTargetSpecification}. Every other step is framework-owned or an optional {@code [default]} - * the domain may override. + * #planTasks}, {@link #analyse} and {@link #mapToTargetSpecification}. Every other step is + * framework-owned or an optional {@code [default]} the domain may override. * *

The round spine: * @@ -66,7 +65,7 @@ * 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 predictTaskResultTypeNames() — implement] + * registerPlannedTaskResults(tasks, ctx) [framework; per-task names from predictTaskResultTypeNames() — default: empty] * loop 1 — specification (guarded, strict-mode reporting): [framework] * defs = analyse(task, ctx) [implement] * target = mapToTargetSpecification(def, cfg, ctx) (per def) [implement] @@ -524,15 +523,17 @@ protected void registerPlannedTaskResults(List> tasks, C context) * planned names (see {@link AnnotationProcessingContext#resolveGeneratedType}) without a * generation-order dependency: analysis of every task already knows every target name. * - *

A domain whose generated types never reference other generated types may return an empty - * list — registration then simply records nothing for the task. Keep this pure: registrations and - * their side effects belong to {@link #onTaskResultRegistered}. + *

A domain whose generated types never reference other generated types needs no prediction — + * registration then simply records nothing for the task (the default). Keep this pure: + * registrations and their side effects belong to {@link #onTaskResultRegistered}. * * @param task the planned task * @param context the processing context * @return the predicted generated type names (empty when the task registers nothing) */ - protected abstract List predictTaskResultTypeNames(CodegenTask task, C context); + protected List predictTaskResultTypeNames(CodegenTask task, C context) { + return List.of(); + } /** * Runs after a predicted type name is registered for a task — the extension point for domain diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java index 77076974..889b83ca 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java @@ -115,15 +115,6 @@ protected List> planTasks( .toList(); } - @Override - protected List predictTaskResultTypeNames( - CodegenTask task, AnnotationProcessingContext ctx) { - if (!(task.element() instanceof TypeElement target)) { - return List.of(); - } - return List.of(builderName(target, ctx)); - } - @Override protected List analyse( CodegenTask task, AnnotationProcessingContext ctx) { From 4ca396a5d582d873e41e78001515bd3cdca41165 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:56:46 +0000 Subject: [PATCH 147/170] Make debug log reflect reality: real file path, per-generator contributions, gated Jackson registration --- .../builders/processor/BuilderProcessor.java | 3 +- .../roaster/RoasterCodeGenerator.java | 5 +- .../integration/JacksonModuleGenerator.java | 3 + .../registry/GeneratorRegistry.java | 55 ++++++++++++++++--- .../processor/BuilderProcessorTest.java | 29 +++++----- 5 files changed, 66 insertions(+), 29 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 2ee787b6..7fa12807 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 @@ -484,9 +484,8 @@ protected void onTaskGenerated( || f.getFieldType() instanceof TypeNameMap) .count(); - // Collect info for Jackson Module if enabled + // Collect info for Jackson Module if enabled (the generator logs each registration) jacksonModuleGenerator.addEntry(definition, generation.task().element()); - ctx.debug("Jackson module entry added"); // End class-level timing (started by the driver before analysis) ctx.getPerformanceTracker() diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index c8763693..026f117f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -562,13 +562,10 @@ private String formatSource(String rawSource, SourceFormatter formatter) { private void writeClassToFile(String sourceCode, GenerationTargetClassDto classDef) throws CodeGenerationException { - logger.debug( - "Writing class to file: %s.%s", - classDef.getTypeName().getPackageName(), classDef.getTypeName().getClassName()); - String qualifiedName = classDef.getTypeName().getFullQualifiedName(); try { JavaFileObject file = processingEnv.getFiler().createSourceFile(qualifiedName); + logger.debug("Writing class to file: %s", file.toUri().getPath()); try (Writer writer = file.openWriter()) { writer.write(sourceCode); } 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 4e732f29..53762940 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 @@ -94,6 +94,9 @@ public void addEntry(BuilderDefinitionDto builderDef, Element sourceElement) { targetPackage, new JacksonModuleEntryDto( builderDef.getBuildingTargetTypeName(), builderDef.getBuilderTypeName())); + logger.debug( + "Registered %s for Jackson module in package '%s'", + builderDef.getBuilderTypeName().getClassName(), targetPackage); } private String getTargetPackage(BuilderConfiguration config, BuilderDefinitionDto builderDef) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java index 484709c3..6f2a2907 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java @@ -101,16 +101,18 @@ public List generateAllMethods( for (MethodGenerator generator : methodGenerators) { if (generator.appliesTo(field, dtoType, context)) { try { - context.debug( - "Applying: %s (priority: %d)", - generator.getClass().getSimpleName(), generator.getPriority()); - String genName = generator.getClass().getSimpleName(); performanceTracker.startGenerator(); List generatedMethods = generator.generateMethods(field, builderType, context); performanceTracker.endGenerator(genName); + context.debug( + "Applied: %s (priority: %d): +%d methods", + genName, + generator.getPriority(), + generatedMethods == null ? 0 : generatedMethods.size()); + if (CollectionUtils.isNotEmpty(generatedMethods)) { allMethods.addAll(generatedMethods); } @@ -138,15 +140,18 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, TypeName dtoType) { for (BuilderEnhancer enhancer : builderEnhancers) { if (enhancer.appliesTo(builderDto, dtoType, context)) { try { - context.debug( - "Applying: %s (priority: %d)", - enhancer.getClass().getSimpleName(), enhancer.getPriority()); - + int[] before = contentCounts(builderDto); String enhName = enhancer.getClass().getSimpleName(); performanceTracker.startEnhancer(); enhancer.enhanceBuilder(builderDto, context); performanceTracker.endEnhancer(enhName); appliedEnhancers++; + + context.debug( + "Applied: %s (priority: %d)%s", + enhName, + enhancer.getPriority(), + describeContribution(contentCounts(builderDto), before)); } catch (Exception e) { context.error( "Failed to apply enhancer %s to builder %s: %s", @@ -164,6 +169,40 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, TypeName dtoType) { } } + /** Counts the content slots of a builder definition an enhancer can contribute to. */ + private static int[] contentCounts(BuilderDefinitionDto builderDto) { + return new int[] { + builderDto.getMethods().size(), + builderDto.getConstructors().size(), + builderDto.getNestedTypes().size(), + builderDto.getClassAnnotations().size(), + builderDto.getInterfaces().size(), + builderDto.getClassJavadoc() == null ? 0 : 1 + }; + } + + /** + * Renders what an enhancer added, e.g. {@code ": +2 methods, +1 constructor"}, or an empty string + * when it changed nothing measurable. + */ + private static String describeContribution(int[] after, int[] before) { + String[] labels = {"methods", "constructors", "nested types", "annotations", "interfaces"}; + StringBuilder contribution = new StringBuilder(); + for (int i = 0; i < labels.length; i++) { + if (after[i] > before[i]) { + contribution + .append(contribution.isEmpty() ? ": +" : ", +") + .append(after[i] - before[i]) + .append(' ') + .append(labels[i]); + } + } + if (after[5] > before[5]) { + contribution.append(contribution.isEmpty() ? ": " : ", ").append("javadoc added"); + } + return contribution.toString(); + } + /** * Loads all available generators from the service loader and separates them into method * generators and builder enhancers. 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 4ec01856..fdfce891 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 @@ -115,25 +115,25 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[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: │ │ │ │ ├─ Applied: BasicSetterGenerator (priority: 100): +1 methods", + "[DEBUG] simple-builders: │ │ │ │ ├─ Applied: StringFormatHelperGenerator (priority: 80): +1 methods", + "[DEBUG] simple-builders: │ │ │ │ ├─ Applied: SupplierMethodGenerator (priority: 60): +1 methods", + "[DEBUG] simple-builders: │ │ │ │ ├─ Applied: StringBuilderConsumerGenerator (priority: 45): +1 methods", + "[DEBUG] simple-builders: │ │ │ │ ├─ Applied: UpdateHelperGenerator (priority: 31): +1 methods", "[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: │ │ ├─ Applied: GeneratedAnnotationEnhancer (priority: 120): +1 annotations", "[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: │ │ ├─ Applied: BuilderImplementationAnnotationEnhancer (priority: 115): +1 annotations", + "[DEBUG] simple-builders: │ │ ├─ Applied: CoreMethodsEnhancer (priority: 100): +3 methods", + "[DEBUG] simple-builders: │ │ ├─ Applied: ConstructorEnhancer (priority: 95): +2 constructors", + "[DEBUG] simple-builders: │ │ ├─ Applied: WithInterfaceEnhancer (priority: 95): +1 nested types", + "[DEBUG] simple-builders: │ │ ├─ Applied: InterfaceEnhancer (priority: 90): +1 interfaces", "[DEBUG] simple-builders: │ │ ├─ Added conditional methods to builder VerboseTestBuilder", - "[DEBUG] simple-builders: │ │ ├─ Applying: ClassJavaDocEnhancer (priority: 10)", + "[DEBUG] simple-builders: │ │ ├─ Applied: ConditionalEnhancer (priority: 80): +2 methods", + "[DEBUG] simple-builders: │ │ ├─ Applied: ClassJavaDocEnhancer (priority: 10): javadoc added", "[DEBUG] simple-builders: │ │ └─ Applied 8 builder enhancers", "[DEBUG] simple-builders: │ ├─ Finalizing builder definition", "[DEBUG] simple-builders: │ │ ├─ Resolving method conflicts", @@ -159,9 +159,8 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[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: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/VerboseTestBuilder.java", "[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: planning is skipped before the domain hook runs From c51fa0bf89a055e0caa3597de365375b30e03ef6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:56:48 +0000 Subject: [PATCH 148/170] Rewrite doc log excerpts as verbatim capture of a real verbose run --- docs/CONTRIBUTING.md | 8 ++- docs/DEBUG_LOGGING.md | 131 +++++++++++++++++++++--------------------- 2 files changed, 71 insertions(+), 68 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index b8c015c9..6cf3a5c3 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -251,9 +251,14 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md). [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: │ │ │ ├─ Processing method generators +[DEBUG] simple-builders: │ │ │ │ ├─ Applied: BasicSetterGenerator (priority: 100): +1 methods +[DEBUG] simple-builders: │ │ │ │ └─ Generated 6 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: │ │ ├─ Applied: CoreMethodsEnhancer (priority: 100): +3 methods +[DEBUG] simple-builders: │ │ ├─ Applied: ConstructorEnhancer (priority: 95): +2 constructors [DEBUG] simple-builders: │ │ └─ Applied 8 builder enhancers [DEBUG] simple-builders: │ ├─ Finalizing builder definition [DEBUG] simple-builders: │ │ └─ Finalized: 1 class fields, 4 builder-level methods, 2 constructors @@ -265,9 +270,8 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md). [DEBUG] simple-builders: │ ├─ Generating 4 method candidates [DEBUG] simple-builders: │ │ ├─ Resolved to 4 methods after conflict resolution [DEBUG] simple-builders: │ │ └─ Methods added: 4 -[DEBUG] simple-builders: │ ├─ Writing class to file: test.ProjectBuilder +[DEBUG] simple-builders: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/ProjectBuilder.java [DEBUG] simple-builders: │ └─ Successfully generated class: ProjectBuilder -[DEBUG] simple-builders: ├─ Jackson module entry added [DEBUG] simple-builders: └─ Generated builder with 1 fields and 4 methods for 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 5f6d747f..8d484986 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -87,7 +87,7 @@ This approach ensures clean production logs while maintaining full debugging cap ## Example Debug Output -When debug logging is enabled, you'll see detailed output with visual separators: +The following is a verbatim excerpt of a real run (`mvn -pl example clean compile -Dsimplebuilder.verbose=true` on the `example` module, slightly truncated where marked with `…`). It follows `SponsorDto`, the smallest annotated class (one field). Every annotated element produces the same step structure — extraction, enhancer pass, finalize, code generation: ``` [INFO] [DEBUG] simple-builders: Starting BuilderProcessor... @@ -97,86 +97,85 @@ When debug logging is enabled, you'll see detailed output with visual separators [INFO] [DEBUG] simple-builders: └─ Initialized GeneratorRegistry with 16 method generators and 10 builder enhancers [INFO] simple-builders: PROCESSING ROUND START [INFO] [DEBUG] simple-builders: Processing round started. -[INFO] [DEBUG] simple-builders: Found 3 annotated elements. -[INFO] [DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor. -[INFO] [DEBUG] simple-builders: Processing element: PersonDto -[INFO] [DEBUG] simple-builders: ├─ Resolving configuration for element: PersonDto +[INFO] [DEBUG] simple-builders: Found 12 annotated elements. +[INFO] [DEBUG] simple-builders: Found 1 type(s) for generation with @SimpleBuilderFor. +[INFO] [DEBUG] simple-builders: Processing element: SponsorDto +[INFO] [DEBUG] simple-builders: ├─ Resolving configuration for element: SponsorDto [INFO] [DEBUG] simple-builders: │ ├─ Built-in template @SimpleBuilder found in DIRECT scope -[INFO] [DEBUG] simple-builders: │ └─ Resulting configuration resolved: BuilderConfiguration[builderSuffix=Builder,...] -[INFO] [DEBUG] simple-builders: Processing element: OrderDto -[INFO] [DEBUG] simple-builders: ├─ Resolving configuration for element: OrderDto -[INFO] [DEBUG] simple-builders: │ └─ Resulting configuration resolved: BuilderConfiguration[builderSuffix=Builder,...] -[INFO] [DEBUG] simple-builders: Processing element: CustomerDto -[INFO] [DEBUG] simple-builders: ├─ Resolving configuration for element: CustomerDto -[INFO] [DEBUG] simple-builders: │ └─ Resulting configuration resolved: BuilderConfiguration[builderSuffix=Builder,...] -[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: │ │ │ ├─ Processing method generators -[INFO] [DEBUG] simple-builders: │ │ │ │ ├─ Applying: BasicSetterGenerator (priority: 100) -[INFO] [DEBUG] simple-builders: │ │ │ │ └─ Generated 1 methods -[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 class based enhancer -[INFO] [DEBUG] simple-builders: │ │ ├─ Applying: CoreMethodsEnhancer (priority: 100) -[INFO] [DEBUG] simple-builders: │ │ ├─ Applying: ConstructorEnhancer (priority: 95) -[INFO] [DEBUG] simple-builders: │ │ ├─ Applying: ClassJavaDocEnhancer (priority: 10) -[INFO] [DEBUG] simple-builders: │ │ └─ Applied 3 builder enhancers -[INFO] [DEBUG] simple-builders: │ ├─ Finalizing builder definition -[INFO] [DEBUG] simple-builders: │ │ ├─ Resolving method conflicts -[INFO] [DEBUG] simple-builders: │ │ │ └─ Resolved: 7 signatures, 0 conflicts, 0 methods removed -[INFO] [DEBUG] simple-builders: │ │ └─ Finalized: 2 class fields, 5 builder-level methods, 2 constructors -[INFO] [DEBUG] simple-builders: │ ├─ Builder will be generated as: PersonDtoBuilder -[INFO] [DEBUG] simple-builders: │ └─ Builder definition extracted: PersonDtoBuilder -[INFO] [DEBUG] simple-builders: Processing element: OrderDto -[INFO] [DEBUG] simple-builders: ├─ Extracting builder definition from: org.example.OrderDto -[INFO] [DEBUG] simple-builders: │ └─ Builder definition extracted: OrderDtoBuilder -[INFO] [DEBUG] simple-builders: Processing element: CustomerDto -[INFO] [DEBUG] simple-builders: ├─ Extracting builder definition from: org.example.CustomerDto -[INFO] [DEBUG] simple-builders: │ └─ Builder definition extracted: CustomerDtoBuilder -[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 class: PersonDtoBuilder +[INFO] [DEBUG] simple-builders: │ └─ Resulting configuration resolved: BuilderConfiguration[generateFieldSupplier=ENABLED,…,builderSuffix=Builder,formattingMode=jdt,strict=DISABLED,verbose=ENABLED] +… (the same configuration-resolution steps repeat for each of the 12 elements) +[INFO] [DEBUG] simple-builders: 12 of 12 annotated element(s) are inside the builderGenerationPackages scope. +[INFO] [DEBUG] simple-builders: ├─ Processing element: SponsorDto +[INFO] [DEBUG] simple-builders: │ ├─ Extracting builder definition from: org.javahelpers.simple.builders.example.SponsorDto +[INFO] [DEBUG] simple-builders: │ │ ├─ Builder will be generated as: org.javahelpers.simple.builders.example.SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: │ │ ├─ Analysing setters for finding fields +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Analyzing method: setName(java.lang.String) +[INFO] [DEBUG] simple-builders: │ │ │ │ ├─ Processing method generators +[INFO] [DEBUG] simple-builders: │ │ │ │ │ ├─ Applied: BasicSetterGenerator (priority: 100): +1 methods +[INFO] [DEBUG] simple-builders: │ │ │ │ │ ├─ Applied: StringFormatHelperGenerator (priority: 80): +1 methods +[INFO] [DEBUG] simple-builders: │ │ │ │ │ ├─ Applied: SupplierMethodGenerator (priority: 60): +1 methods +[INFO] [DEBUG] simple-builders: │ │ │ │ │ ├─ Applied: StringValidationGenerator (priority: 60): +1 methods +[INFO] [DEBUG] simple-builders: │ │ │ │ │ ├─ Applied: StringBuilderConsumerGenerator (priority: 45): +1 methods +[INFO] [DEBUG] simple-builders: │ │ │ │ │ ├─ Applied: UpdateHelperGenerator (priority: 31): +1 methods +[INFO] [DEBUG] simple-builders: │ │ │ │ │ └─ Generated 6 methods +[INFO] [DEBUG] simple-builders: │ │ │ │ └─ Adding field: name (type: java.lang.String) +[INFO] [DEBUG] simple-builders: │ │ │ └─ Processed 1 possible setters: added 1 fields, skipped 0 +[INFO] [DEBUG] simple-builders: │ │ ├─ Processing class based enhancer +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Applied: GeneratedAnnotationEnhancer (priority: 120): +1 annotations +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Added @BuilderImplementation annotation to builder SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Applied: BuilderImplementationAnnotationEnhancer (priority: 115): +1 annotations +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Applied: CoreMethodsEnhancer (priority: 100): +3 methods +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Applied: ConstructorEnhancer (priority: 95): +2 constructors +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Applied: WithInterfaceEnhancer (priority: 95): +1 nested types +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Applied: InterfaceEnhancer (priority: 90): +1 interfaces +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Added conditional methods to builder SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Applied: ConditionalEnhancer (priority: 80): +2 methods +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Applied: ClassJavaDocEnhancer (priority: 10): javadoc added +[INFO] [DEBUG] simple-builders: │ │ │ └─ Applied 8 builder enhancers +[INFO] [DEBUG] simple-builders: │ │ ├─ Finalizing builder definition +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Resolving method conflicts +[INFO] [DEBUG] simple-builders: │ │ │ │ └─ Resolved: 11 signatures, 0 conflicts, 0 methods removed +[INFO] [DEBUG] simple-builders: │ │ │ └─ Finalized: 1 class fields, 5 builder-level methods, 2 constructors +[INFO] [DEBUG] simple-builders: │ │ ├─ Builder will be generated as: SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: │ │ └─ Builder definition extracted: SponsorDtoBuilder +… (the same extraction steps repeat for each remaining element) +[INFO] [DEBUG] simple-builders: Generating targets for: SponsorDto +[INFO] [DEBUG] simple-builders: ├─ Code generation for class: SponsorDtoBuilder [INFO] [DEBUG] simple-builders: │ ├─ JavaClassSource created [INFO] [DEBUG] simple-builders: │ ├─ Class metadata added -[INFO] [DEBUG] simple-builders: │ ├─ Generating 2 fields -[INFO] [DEBUG] simple-builders: │ │ └─ Fields added: 2 fields +[INFO] [DEBUG] simple-builders: │ ├─ Generating 1 fields +[INFO] [DEBUG] simple-builders: │ │ └─ Fields added: 1 fields [INFO] [DEBUG] simple-builders: │ ├─ Generating 2 constructors [INFO] [DEBUG] simple-builders: │ │ └─ Constructors added: 2 -[INFO] [DEBUG] simple-builders: │ ├─ Generating 7 method candidates -[INFO] [DEBUG] simple-builders: │ │ ├─ Resolved to 7 methods after conflict resolution -[INFO] [DEBUG] simple-builders: │ │ └─ Methods added: 7 +[INFO] [DEBUG] simple-builders: │ ├─ Generating 11 method candidates +[INFO] [DEBUG] simple-builders: │ │ ├─ Resolved to 11 methods after conflict resolution +[INFO] [DEBUG] simple-builders: │ │ └─ Methods added: 11 [INFO] [DEBUG] simple-builders: │ ├─ Generating 1 nested type(s) [INFO] [DEBUG] simple-builders: │ │ ├─ Generated nested type: With [INFO] [DEBUG] simple-builders: │ │ └─ Nested types added [INFO] [DEBUG] simple-builders: │ ├─ Class-level annotations added -[INFO] [DEBUG] simple-builders: │ ├─ Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'. -[INFO] [DEBUG] simple-builders: │ ├─ Writing class to file: org.example.PersonDtoBuilder -[INFO] [DEBUG] simple-builders: │ └─ Successfully generated class: PersonDtoBuilder -[INFO] [DEBUG] simple-builders: ├─ Jackson module entry added -[INFO] [DEBUG] simple-builders: └─ Generated builder with 2 fields and 7 methods for PersonDtoBuilder -[INFO] [DEBUG] simple-builders: Generating targets for: OrderDto -[INFO] [DEBUG] simple-builders: ├─ Code generation for class: OrderDtoBuilder -[INFO] [DEBUG] simple-builders: │ └─ Successfully generated class: OrderDtoBuilder -[INFO] [DEBUG] simple-builders: Generating targets for: CustomerDto -[INFO] [DEBUG] simple-builders: ├─ Code generation for class: CustomerDtoBuilder -[INFO] [DEBUG] simple-builders: │ └─ Successfully generated class: CustomerDtoBuilder -[INFO] simple-builders: Successfully generated 3 target(s) in this processing round +[INFO] [DEBUG] simple-builders: │ ├─ Writing class to file: …/example/generated-example-builder/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java +[INFO] [DEBUG] simple-builders: │ └─ Successfully generated class: SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: └─ Generated builder with 1 fields and 11 methods for SponsorDtoBuilder +… (the same code-generation steps repeat for each remaining element; elements whose + configuration enables the Jackson module additionally emit a registration line:) +[INFO] [DEBUG] simple-builders: ├─ Registered JacksonIntegrationDtoBuilder for Jackson module in package 'org.javahelpers.simple.builders.example' +[INFO] simple-builders: Successfully generated 12 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. -[INFO] [DEBUG] simple-builders: Found 0 type(s) for generation with @SimpleBuilderFor. -[INFO] [DEBUG] simple-builders: 0 of 0 annotated element(s) are inside the builderGenerationPackages scope. +[INFO] [DEBUG] simple-builders: No matching elements collected — skipping planning for this round. +… (empty rounds repeat until processing finishes) [INFO] simple-builders: Processing OVER. JacksonModuleEnabled: true, Packages: 1 -[INFO] simple-builders: Generating Jackson module in package 'org.example' +[INFO] simple-builders: Generating Jackson module in package 'org.javahelpers.simple.builders.example' [INFO] [DEBUG] simple-builders: Code generation for class: SimpleBuildersJacksonModule ``` +Notes on individual lines: + +- `Applied: (priority: N): +M ` — logged **after** each generator/enhancer ran, with what it actually contributed (methods, constructors, nested types, annotations, interfaces, or javadoc). An enhancer that adds nothing measurable prints just `Applied: (priority: N)` with no suffix. +- `Writing class to file:` — the real path of the generated source file, resolved via the filer's `JavaFileObject.toUri()`. +- `Registered for Jackson module in package ''` — emitted by the Jackson module generator only when the builder actually registered for module generation (i.e. `generateJacksonModule` enabled on that element's configuration and Jackson on the classpath). It is not a generation step of its own. + **Note**: Debug messages are prefixed with `[DEBUG]` and use `Diagnostic.Kind.OTHER` which appears as `[INFO]` in Maven output. ## Normal Output (Without Debug) From 4aa5d149ed343dcbc05c661db4e746de10a966aa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:57:54 +0000 Subject: [PATCH 149/170] Inline logger-prefix resolution: prefix is configuration, not an extension point --- .../processing/AbstractCodegenProcessor.java | 18 ++++-------------- 1 file changed, 4 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 fae474fc..e884e423 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -320,25 +320,15 @@ private void assertTargetDoesNotExist(GenerationTargetClassDto target) } } - /** - * The diagnostic prefix carried by every emitted log line ("{@code prefix: }" — applied by the - * logger to each message). Default: the {@link CodegenProcessorConfiguration#loggerPrefix()} - * declaration, or the processor's simple class name when unset; override for a dynamic prefix. - * - * @return the logger prefix - */ - protected String provideLoggerPrefix() { - String configured = processorConfiguration.loggerPrefix(); - return configured != null ? configured : getClass().getSimpleName(); - } - /** * Creates the logger: verbose when the domain's declared verbose option requests it — see {@link - * ProcessorOptionRegistry#isVerboseRequested} — and carrying {@link #provideLoggerPrefix()}. + * ProcessorOptionRegistry#isVerboseRequested} — and carrying the prefix from {@link + * CodegenProcessorConfiguration#loggerPrefix()}, or the processor's simple class name when unset. */ private ProcessingLogger createLogger( ProcessingEnvironment processingEnv, CompilerArgumentsReader reader) { - String loggerPrefix = provideLoggerPrefix(); + String configuredPrefix = processorConfiguration.loggerPrefix(); + String loggerPrefix = configuredPrefix != null ? configuredPrefix : getClass().getSimpleName(); ProcessingLogger processingLogger = optionRegistry().isVerboseRequested(reader) ? ProcessingLogger.createVerbose(processingEnv, loggerPrefix) From b3a866436487795d9024ad771c8e6858096f6108 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:12:29 +0000 Subject: [PATCH 150/170] Close the 'Generating targets' operation after onTaskGenerated: summary is a child line, not the op closer --- docs/CONTRIBUTING.md | 2 +- docs/DEBUG_LOGGING.md | 2 +- .../simple/builders/processor/BuilderProcessor.java | 5 +++-- .../simple/builders/processor/BuilderProcessorTest.java | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6cf3a5c3..f97f4eee 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -272,7 +272,7 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md). [DEBUG] simple-builders: │ │ └─ Methods added: 4 [DEBUG] simple-builders: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/ProjectBuilder.java [DEBUG] simple-builders: │ └─ Successfully generated class: ProjectBuilder -[DEBUG] simple-builders: └─ Generated builder with 1 fields and 4 methods for ProjectBuilder +[DEBUG] simple-builders: ├─ Generated builder with 1 fields and 4 methods for 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 8d484986..2690ebe9 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -156,7 +156,7 @@ The following is a verbatim excerpt of a real run (`mvn -pl example clean compil [INFO] [DEBUG] simple-builders: │ ├─ Class-level annotations added [INFO] [DEBUG] simple-builders: │ ├─ Writing class to file: …/example/generated-example-builder/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java [INFO] [DEBUG] simple-builders: │ └─ Successfully generated class: SponsorDtoBuilder -[INFO] [DEBUG] simple-builders: └─ Generated builder with 1 fields and 11 methods for SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: ├─ Generated builder with 1 fields and 11 methods for SponsorDtoBuilder … (the same code-generation steps repeat for each remaining element; elements whose configuration enables the Jackson module additionally emit a registration line:) [INFO] [DEBUG] simple-builders: ├─ Registered JacksonIntegrationDtoBuilder for Jackson module in package 'org.javahelpers.simple.builders.example' 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 7fa12807..e5031b95 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 @@ -491,8 +491,9 @@ protected void onTaskGenerated( ctx.getPerformanceTracker() .endClass(definition.getAllFieldsForBuilder().size(), collectionCount); - // Add summary of what was generated - ctx.debugEndOperation( + // Add summary of what was generated (the driver closes the "Generating targets" operation + // after this step) + ctx.debug( "Generated builder with %d fields and %d methods for %s", definition.getAllFieldsForBuilder().size(), renderingDto.getMethods().size(), 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 fdfce891..2d27b579 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 @@ -161,7 +161,7 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { + " profile 'eclipse-java-format.xml'.", "[DEBUG] simple-builders: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/VerboseTestBuilder.java", "[DEBUG] simple-builders: │ └─ Successfully generated class: VerboseTestBuilder", - "[DEBUG] simple-builders: └─ Generated builder with 1 fields and 10 methods for VerboseTestBuilder", + "[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: planning is skipped before the domain hook runs "simple-builders: PROCESSING ROUND START", From 823c489c5bb95d798c47d579f6b3c020974de0f4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:12:58 +0000 Subject: [PATCH 151/170] Show full Jackson module generation tail in debug log example --- docs/DEBUG_LOGGING.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md index 2690ebe9..7e057317 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -168,6 +168,21 @@ The following is a verbatim excerpt of a real run (`mvn -pl example clean compil [INFO] simple-builders: Processing OVER. JacksonModuleEnabled: true, Packages: 1 [INFO] simple-builders: Generating Jackson module in package 'org.javahelpers.simple.builders.example' [INFO] [DEBUG] simple-builders: Code generation for class: SimpleBuildersJacksonModule +[INFO] [DEBUG] simple-builders: ├─ JavaClassSource created +[INFO] [DEBUG] simple-builders: ├─ Class metadata added +[INFO] [DEBUG] simple-builders: ├─ Generating 0 fields +[INFO] [DEBUG] simple-builders: │ └─ Fields added: 0 fields +[INFO] [DEBUG] simple-builders: ├─ Generating 1 constructors +[INFO] [DEBUG] simple-builders: │ └─ Constructors added: 1 +[INFO] [DEBUG] simple-builders: ├─ Generating 0 method candidates +[INFO] [DEBUG] simple-builders: │ ├─ Resolved to 0 methods after conflict resolution +[INFO] [DEBUG] simple-builders: │ └─ Methods added: 0 +[INFO] [DEBUG] simple-builders: ├─ Generating 1 nested type(s) +[INFO] [DEBUG] simple-builders: │ ├─ Generated nested type: JacksonIntegrationDtoMixin +[INFO] [DEBUG] simple-builders: │ └─ Nested types added +[WARNING] File for type 'org.javahelpers.simple.builders.example.SimpleBuildersJacksonModule' created in the last round will not be subject to annotation processing. +[INFO] [DEBUG] simple-builders: ├─ Writing class to file: …/example/generated-example-builder/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java +[INFO] [DEBUG] simple-builders: └─ Successfully generated class: SimpleBuildersJacksonModule ``` Notes on individual lines: From de06203ca98e7f91689393b9325469a0a0769316 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:14:54 +0000 Subject: [PATCH 152/170] Log before creating JavaFileObject so a filer failure has a preceding trace line --- docs/CONTRIBUTING.md | 1 + docs/DEBUG_LOGGING.md | 2 ++ .../processor/classgen/roaster/RoasterCodeGenerator.java | 1 + .../simple/builders/processor/BuilderProcessorTest.java | 1 + 4 files changed, 5 insertions(+) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f97f4eee..354a1fce 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -270,6 +270,7 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md). [DEBUG] simple-builders: │ ├─ Generating 4 method candidates [DEBUG] simple-builders: │ │ ├─ Resolved to 4 methods after conflict resolution [DEBUG] simple-builders: │ │ └─ Methods added: 4 +[DEBUG] simple-builders: │ ├─ Creating JavaFileObject: test.ProjectBuilder [DEBUG] simple-builders: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/ProjectBuilder.java [DEBUG] simple-builders: │ └─ Successfully generated class: ProjectBuilder [DEBUG] simple-builders: ├─ Generated builder with 1 fields and 4 methods for ProjectBuilder diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md index 7e057317..2e01b95a 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -154,6 +154,7 @@ The following is a verbatim excerpt of a real run (`mvn -pl example clean compil [INFO] [DEBUG] simple-builders: │ │ ├─ Generated nested type: With [INFO] [DEBUG] simple-builders: │ │ └─ Nested types added [INFO] [DEBUG] simple-builders: │ ├─ Class-level annotations added +[INFO] [DEBUG] simple-builders: │ ├─ Creating JavaFileObject: org.javahelpers.simple.builders.example.SponsorDtoBuilder [INFO] [DEBUG] simple-builders: │ ├─ Writing class to file: …/example/generated-example-builder/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java [INFO] [DEBUG] simple-builders: │ └─ Successfully generated class: SponsorDtoBuilder [INFO] [DEBUG] simple-builders: ├─ Generated builder with 1 fields and 11 methods for SponsorDtoBuilder @@ -181,6 +182,7 @@ The following is a verbatim excerpt of a real run (`mvn -pl example clean compil [INFO] [DEBUG] simple-builders: │ ├─ Generated nested type: JacksonIntegrationDtoMixin [INFO] [DEBUG] simple-builders: │ └─ Nested types added [WARNING] File for type 'org.javahelpers.simple.builders.example.SimpleBuildersJacksonModule' created in the last round will not be subject to annotation processing. +[INFO] [DEBUG] simple-builders: ├─ Creating JavaFileObject: org.javahelpers.simple.builders.example.SimpleBuildersJacksonModule [INFO] [DEBUG] simple-builders: ├─ Writing class to file: …/example/generated-example-builder/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java [INFO] [DEBUG] simple-builders: └─ Successfully generated class: SimpleBuildersJacksonModule ``` diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 026f117f..7e7a7e2e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -563,6 +563,7 @@ private String formatSource(String rawSource, SourceFormatter formatter) { private void writeClassToFile(String sourceCode, GenerationTargetClassDto classDef) throws CodeGenerationException { String qualifiedName = classDef.getTypeName().getFullQualifiedName(); + logger.debug("Creating JavaFileObject: %s", qualifiedName); try { JavaFileObject file = processingEnv.getFiler().createSourceFile(qualifiedName); logger.debug("Writing class to file: %s", file.toUri().getPath()); 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 2d27b579..e1465d94 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 @@ -159,6 +159,7 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[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: │ ├─ Creating JavaFileObject: test.VerboseTestBuilder", "[DEBUG] simple-builders: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/VerboseTestBuilder.java", "[DEBUG] simple-builders: │ └─ Successfully generated class: VerboseTestBuilder", "[DEBUG] simple-builders: ├─ Generated builder with 1 fields and 10 methods for VerboseTestBuilder", From dd9c3c87906992681858a5883bde7de0c230db44 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:19:09 +0000 Subject: [PATCH 153/170] Skip codegen phase logging when the phase has nothing to emit --- docs/DEBUG_LOGGING.md | 6 +-- .../roaster/RoasterCodeGenerator.java | 42 ++++++++++--------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md index 2e01b95a..5085e672 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -171,13 +171,8 @@ The following is a verbatim excerpt of a real run (`mvn -pl example clean compil [INFO] [DEBUG] simple-builders: Code generation for class: SimpleBuildersJacksonModule [INFO] [DEBUG] simple-builders: ├─ JavaClassSource created [INFO] [DEBUG] simple-builders: ├─ Class metadata added -[INFO] [DEBUG] simple-builders: ├─ Generating 0 fields -[INFO] [DEBUG] simple-builders: │ └─ Fields added: 0 fields [INFO] [DEBUG] simple-builders: ├─ Generating 1 constructors [INFO] [DEBUG] simple-builders: │ └─ Constructors added: 1 -[INFO] [DEBUG] simple-builders: ├─ Generating 0 method candidates -[INFO] [DEBUG] simple-builders: │ ├─ Resolved to 0 methods after conflict resolution -[INFO] [DEBUG] simple-builders: │ └─ Methods added: 0 [INFO] [DEBUG] simple-builders: ├─ Generating 1 nested type(s) [INFO] [DEBUG] simple-builders: │ ├─ Generated nested type: JacksonIntegrationDtoMixin [INFO] [DEBUG] simple-builders: │ └─ Nested types added @@ -192,6 +187,7 @@ Notes on individual lines: - `Applied: (priority: N): +M ` — logged **after** each generator/enhancer ran, with what it actually contributed (methods, constructors, nested types, annotations, interfaces, or javadoc). An enhancer that adds nothing measurable prints just `Applied: (priority: N)` with no suffix. - `Writing class to file:` — the real path of the generated source file, resolved via the filer's `JavaFileObject.toUri()`. - `Registered for Jackson module in package ''` — emitted by the Jackson module generator only when the builder actually registered for module generation (i.e. `generateJacksonModule` enabled on that element's configuration and Jackson on the classpath). It is not a generation step of its own. +- Code-generation phases with nothing to emit log nothing — a class without fields, methods or nested types (like `SimpleBuildersJacksonModule`, which only has a constructor and a mixin) produces no `Generating 0 …` / `added: 0` noise. **Note**: Debug messages are prefixed with `[DEBUG]` and use `Diagnostic.Kind.OTHER` which appears as `[INFO]` in Maven output. diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 7e7a7e2e..c69be610 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -222,13 +222,13 @@ private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto c private void appendFields(JavaClassSource source, GenerationTargetClassDto classDef) { performanceTracker.startPhase(); - logger.debugStartOperation("Generating %d fields", classDef.getClassFields().size()); - - for (ClassFieldDto fieldDto : classDef.getClassFields()) { - appendField(source, fieldDto); + if (CollectionUtils.isNotEmpty(classDef.getClassFields())) { + logger.debugStartOperation("Generating %d fields", classDef.getClassFields().size()); + for (ClassFieldDto fieldDto : classDef.getClassFields()) { + appendField(source, fieldDto); + } + logger.debugEndOperation("Fields added: %d fields", source.getFields().size()); } - - logger.debugEndOperation("Fields added: %d fields", source.getFields().size()); performanceTracker.endPhase(PHASE_FIELDS); } @@ -243,13 +243,13 @@ private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { private void appendConstructors(JavaClassSource source, GenerationTargetClassDto classDef) { performanceTracker.startPhase(); - logger.debugStartOperation("Generating %d constructors", classDef.getConstructors().size()); - - for (ConstructorDto constructor : classDef.getConstructors()) { - appendConstructor(source, constructor); + if (CollectionUtils.isNotEmpty(classDef.getConstructors())) { + logger.debugStartOperation("Generating %d constructors", classDef.getConstructors().size()); + for (ConstructorDto constructor : classDef.getConstructors()) { + appendConstructor(source, constructor); + } + logger.debugEndOperation("Constructors added: %d", classDef.getConstructors().size()); } - - logger.debugEndOperation("Constructors added: %d", classDef.getConstructors().size()); performanceTracker.endPhase(PHASE_CONSTRUCTORS); } @@ -267,17 +267,19 @@ private void appendConstructor(JavaClassSource source, ConstructorDto constructo private void appendMethods(JavaClassSource source, GenerationTargetClassDto classDef) { performanceTracker.startPhase(); - logger.debugStartOperation("Generating %d method candidates", classDef.getMethods().size()); + if (CollectionUtils.isNotEmpty(classDef.getMethods())) { + logger.debugStartOperation("Generating %d method candidates", classDef.getMethods().size()); - // Resolve method conflicts by signature and priority - List resolvedMethods = resolveMethodConflicts(classDef.getMethods()); - logger.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size()); + // Resolve method conflicts by signature and priority + List resolvedMethods = resolveMethodConflicts(classDef.getMethods()); + logger.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size()); - for (MethodDto methodDto : resolvedMethods) { - appendMethod(source, methodDto, false, false); - } + for (MethodDto methodDto : resolvedMethods) { + appendMethod(source, methodDto, false, false); + } - logger.debugEndOperation("Methods added: %d", resolvedMethods.size()); + logger.debugEndOperation("Methods added: %d", resolvedMethods.size()); + } performanceTracker.endPhase(PHASE_METHODS); } From 2e37f0323bde7d635ddd018392b7f7d7fff38167 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:36:39 +0000 Subject: [PATCH 154/170] CodegenTask builder: preassign reportingElement default and require non-null fields --- .../codegen/processing/CodegenTask.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) 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 7939bd70..13da4291 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java @@ -24,6 +24,7 @@ package org.javahelpers.codegen.processing; +import java.util.Objects; import javax.lang.model.element.Element; /** @@ -38,8 +39,8 @@ * @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 pass the task to {@link + * @param outputPackage the package generated code is written to — planning computes it so the + * driver can pass the task to {@link * org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext#setCurrentTask} * before analysis */ @@ -63,9 +64,12 @@ public static final class Builder { private Element reportingElement; private String outputPackage; - /** The element the task generates for. */ + /** The element the task generates for; also the default {@link #reportingElement}. */ public Builder element(Element element) { this.element = element; + if (this.reportingElement == null) { + this.reportingElement = element; + } return this; } @@ -81,7 +85,7 @@ public Builder reportingElement(Element reportingElement) { return this; } - /** The package generated code is written to, or {@code null}. */ + /** The package generated code is written to. */ public Builder outputPackage(String outputPackage) { this.outputPackage = outputPackage; return this; @@ -90,10 +94,10 @@ public Builder outputPackage(String outputPackage) { /** Builds the task; the reporting element defaults to the task element. */ public CodegenTask build() { return new CodegenTask<>( - element, - configuration, - reportingElement != null ? reportingElement : element, - outputPackage); + Objects.requireNonNull(element, "element"), + Objects.requireNonNull(configuration, "configuration"), + Objects.requireNonNull(reportingElement, "reportingElement"), + Objects.requireNonNull(outputPackage, "outputPackage")); } } } From 41103c8de7031f5198019c5d0e01fdb666c835a5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:36:39 +0000 Subject: [PATCH 155/170] Require groupName when building an ElementSelector --- .../org/javahelpers/codegen/processing/ElementSelector.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 2b7f82dd..77b19043 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/ElementSelector.java @@ -89,7 +89,11 @@ public Builder optOutAnnotation(Class optOutAnnotation) { /** Builds the selector. */ public ElementSelector build() { - return new ElementSelector(groupName, annotationTypes, metaMarker, optOutAnnotation); + return new ElementSelector( + java.util.Objects.requireNonNull(groupName, "groupName"), + annotationTypes, + metaMarker, + optOutAnnotation); } } } From 5d69428844a5fda9ecbf8f6b0bf8c73edee85a8d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:36:45 +0000 Subject: [PATCH 156/170] Normalize trailing spaces in expected report and extract metric assert helpers --- .../logging/ActivePerformanceTrackerTest.java | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) 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 80730209..37000b68 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.HashSet; import java.util.Set; import org.javahelpers.simple.builders.processor.classgen.GenerationPhases; import org.javahelpers.simple.builders.processor.processing.PipelinePhases; @@ -74,6 +75,35 @@ private JsonNode generateReportAndParseJson(String outputFile) throws IOExceptio return new ObjectMapper().readTree(jsonContent); } + /** The {@code className} values of all class-metric entries. */ + private static Set classNames(JsonNode classMetrics) { + Set names = new HashSet<>(); + for (JsonNode cm : classMetrics) { + names.add(cm.get("className").asText()); + } + return names; + } + + /** Asserts the metric entries are ordered by {@code elapsedNanos} descending. */ + private static void assertSortedByElapsedDesc(JsonNode metrics) { + for (int i = 1; i < metrics.size(); i++) { + assertTrue( + metrics.get(i - 1).get("elapsedNanos").asLong() + >= metrics.get(i).get("elapsedNanos").asLong(), + "metrics not sorted by elapsedNanos descending at index " + i); + } + } + + /** The class-metric entry for the given class name — fails the test when absent. */ + private static JsonNode classMetricByName(JsonNode root, String className) { + for (JsonNode cm : root.get("classMetrics")) { + if (className.equals(cm.get("className").asText())) { + return cm; + } + } + return fail("No classMetric entry for " + className); + } + /** * 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, @@ -104,6 +134,7 @@ void generateReport_prefixedLogger_producesExpectedTaggedLayout() { capturing.messages().stream() .map(m -> m.replaceFirst("^[A-Z]+: ", "")) .map(m -> m.replaceAll("\\d+(\\.\\d+)?", "")) + .map(String::stripTrailing) .toList()); assertEquals( @@ -112,7 +143,7 @@ void generateReport_prefixedLogger_producesExpectedTaggedLayout() { simple-builders: ================================ simple-builders: Total classes processed: simple-builders: Total processing time: s - simple-builders:\s + simple-builders: simple-builders: Phase breakdown: simple-builders: ├─ Element Collection: s (%) simple-builders: ├─ Configuration Resolution: s (%) @@ -131,15 +162,15 @@ void generateReport_prefixedLogger_producesExpectedTaggedLayout() { simple-builders: │ ├─ String Generation: s (%) simple-builders: │ └─ Formatting: s (%) simple-builders: └─ File Writing: s (%) - simple-builders:\s + simple-builders: simple-builders: Average per class: ms - simple-builders:\s + simple-builders: simple-builders: Top slowest classes: simple-builders: . MyClass - ms ( fields, collections) - simple-builders:\s + simple-builders: simple-builders: Top slowest MethodGenerators: simple-builders: . MyGenerator - s ( calls, ms/call) - simple-builders:\s + simple-builders: simple-builders: Top slowest BuilderEnhancers: simple-builders: . MyEnhancer - s ( calls, ms/call)\ """, @@ -564,13 +595,9 @@ void jsonReport_generatorAndEnhancerStats_sortedByElapsedDesc() throws IOExcepti JsonNode root = new ObjectMapper().readTree(Files.readString(jsonFile)); JsonNode genStats = root.get("generatorStats"); - long first = genStats.get(0).get("elapsedNanos").asLong(); - long second = genStats.get(1).get("elapsedNanos").asLong(); - assertTrue(first >= second, "Generator stats should be sorted by elapsed descending"); + assertSortedByElapsedDesc(genStats); JsonNode enhStats = root.get("enhancerStats"); - long firstE = enhStats.get(0).get("elapsedNanos").asLong(); - long secondE = enhStats.get(1).get("elapsedNanos").asLong(); - assertTrue(firstE >= secondE, "Enhancer stats should be sorted by elapsed descending"); + assertSortedByElapsedDesc(enhStats); } } From 87342933a3df83091dec4c72a185ccb7f66e73f1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:36:45 +0000 Subject: [PATCH 157/170] Let the post-generation summary close the generation scope --- docs/CONTRIBUTING.md | 4 +- docs/DEBUG_LOGGING.md | 50 ++++++++++--------- .../processing/AbstractCodegenProcessor.java | 15 +++++- .../builders/processor/BuilderProcessor.java | 8 +-- .../roaster/RoasterCodeGenerator.java | 5 +- .../processor/BuilderProcessorTest.java | 5 +- 6 files changed, 53 insertions(+), 34 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 354a1fce..6b8eed9c 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -272,8 +272,8 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md). [DEBUG] simple-builders: │ │ └─ Methods added: 4 [DEBUG] simple-builders: │ ├─ Creating JavaFileObject: test.ProjectBuilder [DEBUG] simple-builders: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/ProjectBuilder.java -[DEBUG] simple-builders: │ └─ Successfully generated class: ProjectBuilder -[DEBUG] simple-builders: ├─ Generated builder with 1 fields and 4 methods for ProjectBuilder +[DEBUG] simple-builders: │ ├─ Successfully generated class: ProjectBuilder +[DEBUG] simple-builders: │ └─ Generated builder with 1 fields and 4 methods for 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 5085e672..d44c1ca7 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -139,28 +139,29 @@ The following is a verbatim excerpt of a real run (`mvn -pl example clean compil [INFO] [DEBUG] simple-builders: │ │ ├─ Builder will be generated as: SponsorDtoBuilder [INFO] [DEBUG] simple-builders: │ │ └─ Builder definition extracted: SponsorDtoBuilder … (the same extraction steps repeat for each remaining element) -[INFO] [DEBUG] simple-builders: Generating targets for: SponsorDto -[INFO] [DEBUG] simple-builders: ├─ Code generation for class: SponsorDtoBuilder -[INFO] [DEBUG] simple-builders: │ ├─ JavaClassSource created -[INFO] [DEBUG] simple-builders: │ ├─ Class metadata added -[INFO] [DEBUG] simple-builders: │ ├─ Generating 1 fields -[INFO] [DEBUG] simple-builders: │ │ └─ Fields added: 1 fields -[INFO] [DEBUG] simple-builders: │ ├─ Generating 2 constructors -[INFO] [DEBUG] simple-builders: │ │ └─ Constructors added: 2 -[INFO] [DEBUG] simple-builders: │ ├─ Generating 11 method candidates -[INFO] [DEBUG] simple-builders: │ │ ├─ Resolved to 11 methods after conflict resolution -[INFO] [DEBUG] simple-builders: │ │ └─ Methods added: 11 -[INFO] [DEBUG] simple-builders: │ ├─ Generating 1 nested type(s) -[INFO] [DEBUG] simple-builders: │ │ ├─ Generated nested type: With -[INFO] [DEBUG] simple-builders: │ │ └─ Nested types added -[INFO] [DEBUG] simple-builders: │ ├─ Class-level annotations added -[INFO] [DEBUG] simple-builders: │ ├─ Creating JavaFileObject: org.javahelpers.simple.builders.example.SponsorDtoBuilder -[INFO] [DEBUG] simple-builders: │ ├─ Writing class to file: …/example/generated-example-builder/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java -[INFO] [DEBUG] simple-builders: │ └─ Successfully generated class: SponsorDtoBuilder -[INFO] [DEBUG] simple-builders: ├─ Generated builder with 1 fields and 11 methods for SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: ├─ Generating targets for: SponsorDto +[INFO] [DEBUG] simple-builders: │ ├─ Code generation for class: SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: │ │ ├─ JavaClassSource created +[INFO] [DEBUG] simple-builders: │ │ ├─ Class metadata added +[INFO] [DEBUG] simple-builders: │ │ ├─ Generating 1 fields +[INFO] [DEBUG] simple-builders: │ │ │ └─ Fields added: 1 fields +[INFO] [DEBUG] simple-builders: │ │ ├─ Generating 2 constructors +[INFO] [DEBUG] simple-builders: │ │ │ └─ Constructors added: 2 +[INFO] [DEBUG] simple-builders: │ │ ├─ Generating 11 method candidates +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Resolved to 11 methods after conflict resolution +[INFO] [DEBUG] simple-builders: │ │ │ └─ Methods added: 11 +[INFO] [DEBUG] simple-builders: │ │ ├─ Generating 1 nested type(s) +[INFO] [DEBUG] simple-builders: │ │ │ ├─ Generated nested type: With +[INFO] [DEBUG] simple-builders: │ │ │ └─ Nested types added +[INFO] [DEBUG] simple-builders: │ │ ├─ Class-level annotations added +[INFO] [DEBUG] simple-builders: │ │ ├─ Creating JavaFileObject: org.javahelpers.simple.builders.example.SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: │ │ ├─ Writing class to file: …/example/generated-example-builder/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java +[INFO] [DEBUG] simple-builders: │ │ ├─ Successfully generated class: SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: │ │ └─ Generated builder with 1 fields and 11 methods for SponsorDtoBuilder … (the same code-generation steps repeat for each remaining element; elements whose - configuration enables the Jackson module additionally emit a registration line:) -[INFO] [DEBUG] simple-builders: ├─ Registered JacksonIntegrationDtoBuilder for Jackson module in package 'org.javahelpers.simple.builders.example' + configuration enables the Jackson module additionally emit a registration line inside the + same generation scope:) +[INFO] [DEBUG] simple-builders: │ │ ├─ Registered JacksonIntegrationDtoBuilder for Jackson module in package 'org.javahelpers.simple.builders.example' [INFO] simple-builders: Successfully generated 12 target(s) in this processing round [INFO] simple-builders: PROCESSING ROUND START [INFO] [DEBUG] simple-builders: Processing round started. @@ -176,17 +177,18 @@ The following is a verbatim excerpt of a real run (`mvn -pl example clean compil [INFO] [DEBUG] simple-builders: ├─ Generating 1 nested type(s) [INFO] [DEBUG] simple-builders: │ ├─ Generated nested type: JacksonIntegrationDtoMixin [INFO] [DEBUG] simple-builders: │ └─ Nested types added -[WARNING] File for type 'org.javahelpers.simple.builders.example.SimpleBuildersJacksonModule' created in the last round will not be subject to annotation processing. [INFO] [DEBUG] simple-builders: ├─ Creating JavaFileObject: org.javahelpers.simple.builders.example.SimpleBuildersJacksonModule +[WARNING] File for type 'org.javahelpers.simple.builders.example.SimpleBuildersJacksonModule' created in the last round will not be subject to annotation processing. [INFO] [DEBUG] simple-builders: ├─ Writing class to file: …/example/generated-example-builder/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java -[INFO] [DEBUG] simple-builders: └─ Successfully generated class: SimpleBuildersJacksonModule +[INFO] [DEBUG] simple-builders: ├─ Successfully generated class: SimpleBuildersJacksonModule ``` Notes on individual lines: - `Applied: (priority: N): +M ` — logged **after** each generator/enhancer ran, with what it actually contributed (methods, constructors, nested types, annotations, interfaces, or javadoc). An enhancer that adds nothing measurable prints just `Applied: (priority: N)` with no suffix. - `Writing class to file:` — the real path of the generated source file, resolved via the filer's `JavaFileObject.toUri()`. -- `Registered for Jackson module in package ''` — emitted by the Jackson module generator only when the builder actually registered for module generation (i.e. `generateJacksonModule` enabled on that element's configuration and Jackson on the classpath). It is not a generation step of its own. +- `Registered for Jackson module in package ''` — emitted inside the class's generation scope by the Jackson module generator, only when the builder actually registered for module generation (i.e. `generateJacksonModule` enabled on that element's configuration and Jackson on the classpath). It is not a generation step of its own. +- `Generated builder with N fields and M methods for ` — the domain's summary line; emitted as the closing line (`└─`) of the `Code generation for class` scope, which stays open until the post-generation step ran. - Code-generation phases with nothing to emit log nothing — a class without fields, methods or nested types (like `SimpleBuildersJacksonModule`, which only has a constructor and a mixin) produces no `Generating 0 …` / `added: 0` noise. **Note**: Debug messages are prefixed with `[DEBUG]` and use `Diagnostic.Kind.OTHER` which appears as `[INFO]` in Maven output. 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 e884e423..10e22e20 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -216,6 +216,9 @@ public final boolean process(Set annotations, RoundEnviro // Second loop: generation — every specification exists before any file is written. int successfulGenerations = 0; for (PlannedGeneration work : planned) { + // generateTarget leaves its generation-scope operation open so onTaskGenerated can close it + // with a summary line — restore the pre-task indentation whatever the domain did. + int preTaskIndentation = logger.getCurrentIndentationLevel(); context.debugStartOperation( "Generating targets for: " + work.task().element().getSimpleName()); try { @@ -229,7 +232,9 @@ public final boolean process(Set annotations, RoundEnviro context.reportBasedOnStrictMode( work.task().reportingElement(), "Failed to generate target - %s", ex.getMessage()); } finally { - context.debugEndOperation(); + while (logger.getCurrentIndentationLevel() > preTaskIndentation) { + context.debugEndOperation(); + } } } @@ -279,6 +284,10 @@ public SourceVersion getSupportedSourceVersion() { * already exist. Also usable by the domain's {@link #onProcessingOver} implementation for * last-round outputs. * + *

The engine leaves its generation-scope debug operation open so the caller can close it with + * a summary line (see {@link #onTaskGenerated}); callers outside the task driver must close it + * with {@code context.debugEndOperation()}. + * * @param target the class definition to generate * @throws CodeGenerationException if the target exists or generation fails */ @@ -605,6 +614,10 @@ protected abstract GenerationTargetClassDto mapToTargetSpecification( * Event after a task's target generated successfully — the insert point for per-class metrics and * domain bookkeeping. Default: nothing. * + *

Runs while the generation scope is still open — close it with {@code + * context.debugEndOperation(summary)} for a closing summary line; the driver closes whatever + * remains open. + * * @param generation the planned generation — task, analysed definition and generated target * @param context the processing 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 e5031b95..d2c4c4f8 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 @@ -491,9 +491,8 @@ protected void onTaskGenerated( ctx.getPerformanceTracker() .endClass(definition.getAllFieldsForBuilder().size(), collectionCount); - // Add summary of what was generated (the driver closes the "Generating targets" operation - // after this step) - ctx.debug( + // Close the generation scope with the summary of what was generated + ctx.debugEndOperation( "Generated builder with %d fields and %d methods for %s", definition.getAllFieldsForBuilder().size(), renderingDto.getMethods().size(), @@ -525,6 +524,9 @@ protected void onProcessingOver(BuilderProcessingContext ctx) { ctx.reportBasedOnStrictMode( "Error generating Jackson module for package %s: %s", moduleDefinition.getTypeName().getPackageName(), e.getMessage()); + } finally { + // generateTarget leaves its generation-scope operation open — close it here + ctx.debugEndOperation(); } } // Reset indentation after Jackson module generation as well diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index c69be610..263fcaa4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -147,8 +147,9 @@ public void generateClass(GenerationTargetClassDto classDef, SourceFormatter for writeClassToFile(sourceCode, classDef); performanceTracker.endPhase(PHASE_FILE_WRITING); - logger.debugEndOperation( - "Successfully generated class: %s", classDef.getTypeName().getClassName()); + // The generation scope stays open — the caller's post-generation step (onTaskGenerated in the + // task driver) closes it with its summary line (see AbstractCodegenProcessor.generateTarget). + logger.debug("Successfully generated class: %s", classDef.getTypeName().getClassName()); } private JavaClassSource buildClassSource(GenerationTargetClassDto classDef) { 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 e1465d94..d84a248a 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 @@ -161,8 +161,9 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { + " profile 'eclipse-java-format.xml'.", "[DEBUG] simple-builders: │ ├─ Creating JavaFileObject: test.VerboseTestBuilder", "[DEBUG] simple-builders: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/VerboseTestBuilder.java", - "[DEBUG] simple-builders: │ └─ Successfully generated class: VerboseTestBuilder", - "[DEBUG] simple-builders: ├─ Generated builder with 1 fields and 10 methods for VerboseTestBuilder", + "[DEBUG] simple-builders: │ ├─ Successfully generated class: VerboseTestBuilder", + "[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: planning is skipped before the domain hook runs "simple-builders: PROCESSING ROUND START", From d0028519120759f7ffb713d7f2a8a50719350f50 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:41:57 +0000 Subject: [PATCH 158/170] Emit the generation summary as a child line inside the open scope --- docs/CONTRIBUTING.md | 2 +- docs/DEBUG_LOGGING.md | 4 ++-- .../processing/AbstractCodegenProcessor.java | 16 ++++++++-------- .../builders/processor/BuilderProcessor.java | 5 +++-- .../builders/processor/BuilderProcessorTest.java | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6b8eed9c..b143e010 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -273,7 +273,7 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md). [DEBUG] simple-builders: │ ├─ Creating JavaFileObject: test.ProjectBuilder [DEBUG] simple-builders: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/ProjectBuilder.java [DEBUG] simple-builders: │ ├─ Successfully generated class: ProjectBuilder -[DEBUG] simple-builders: │ └─ Generated builder with 1 fields and 4 methods for ProjectBuilder +[DEBUG] simple-builders: │ ├─ Generated builder with 1 fields and 4 methods for 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 d44c1ca7..253ecff7 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -157,7 +157,7 @@ The following is a verbatim excerpt of a real run (`mvn -pl example clean compil [INFO] [DEBUG] simple-builders: │ │ ├─ Creating JavaFileObject: org.javahelpers.simple.builders.example.SponsorDtoBuilder [INFO] [DEBUG] simple-builders: │ │ ├─ Writing class to file: …/example/generated-example-builder/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java [INFO] [DEBUG] simple-builders: │ │ ├─ Successfully generated class: SponsorDtoBuilder -[INFO] [DEBUG] simple-builders: │ │ └─ Generated builder with 1 fields and 11 methods for SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: │ │ ├─ Generated builder with 1 fields and 11 methods for SponsorDtoBuilder … (the same code-generation steps repeat for each remaining element; elements whose configuration enables the Jackson module additionally emit a registration line inside the same generation scope:) @@ -188,7 +188,7 @@ Notes on individual lines: - `Applied: (priority: N): +M ` — logged **after** each generator/enhancer ran, with what it actually contributed (methods, constructors, nested types, annotations, interfaces, or javadoc). An enhancer that adds nothing measurable prints just `Applied: (priority: N)` with no suffix. - `Writing class to file:` — the real path of the generated source file, resolved via the filer's `JavaFileObject.toUri()`. - `Registered for Jackson module in package ''` — emitted inside the class's generation scope by the Jackson module generator, only when the builder actually registered for module generation (i.e. `generateJacksonModule` enabled on that element's configuration and Jackson on the classpath). It is not a generation step of its own. -- `Generated builder with N fields and M methods for ` — the domain's summary line; emitted as the closing line (`└─`) of the `Code generation for class` scope, which stays open until the post-generation step ran. +- `Generated builder with N fields and M methods for ` — the domain's summary line, emitted as the last child (`├─`) inside the `Code generation for class` scope, which stays open until the post-generation step ran and then closes silently. - Code-generation phases with nothing to emit log nothing — a class without fields, methods or nested types (like `SimpleBuildersJacksonModule`, which only has a constructor and a mixin) produces no `Generating 0 …` / `added: 0` noise. **Note**: Debug messages are prefixed with `[DEBUG]` and use `Diagnostic.Kind.OTHER` which appears as `[INFO]` in Maven output. 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 10e22e20..7bcd8265 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -216,8 +216,8 @@ public final boolean process(Set annotations, RoundEnviro // Second loop: generation — every specification exists before any file is written. int successfulGenerations = 0; for (PlannedGeneration work : planned) { - // generateTarget leaves its generation-scope operation open so onTaskGenerated can close it - // with a summary line — restore the pre-task indentation whatever the domain did. + // generateTarget leaves its generation-scope operation open so onTaskGenerated can emit + // its summary inside it — restore the pre-task indentation whatever the domain did. int preTaskIndentation = logger.getCurrentIndentationLevel(); context.debugStartOperation( "Generating targets for: " + work.task().element().getSimpleName()); @@ -284,9 +284,9 @@ public SourceVersion getSupportedSourceVersion() { * already exist. Also usable by the domain's {@link #onProcessingOver} implementation for * last-round outputs. * - *

The engine leaves its generation-scope debug operation open so the caller can close it with - * a summary line (see {@link #onTaskGenerated}); callers outside the task driver must close it - * with {@code context.debugEndOperation()}. + *

The engine leaves its generation-scope debug operation open so the caller can still emit + * inside it (see {@link #onTaskGenerated}); callers outside the task driver must close it with + * {@code context.debugEndOperation()}. * * @param target the class definition to generate * @throws CodeGenerationException if the target exists or generation fails @@ -614,9 +614,9 @@ protected abstract GenerationTargetClassDto mapToTargetSpecification( * Event after a task's target generated successfully — the insert point for per-class metrics and * domain bookkeeping. Default: nothing. * - *

Runs while the generation scope is still open — close it with {@code - * context.debugEndOperation(summary)} for a closing summary line; the driver closes whatever - * remains open. + *

Runs while the generation scope is still open — emit the domain's summary with {@code + * context.debug(...)} as a child line (or {@code context.debugEndOperation(summary)} for a + * closing line); the driver closes whatever remains open. * * @param generation the planned generation — task, analysed definition and generated target * @param context the processing 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 d2c4c4f8..bd2ee4ce 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 @@ -491,8 +491,9 @@ protected void onTaskGenerated( ctx.getPerformanceTracker() .endClass(definition.getAllFieldsForBuilder().size(), collectionCount); - // Close the generation scope with the summary of what was generated - ctx.debugEndOperation( + // Summary of what was generated, inside the still-open generation scope — the driver + // closes the scope silently afterwards + ctx.debug( "Generated builder with %d fields and %d methods for %s", definition.getAllFieldsForBuilder().size(), renderingDto.getMethods().size(), 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 d84a248a..81ecf68a 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 @@ -162,7 +162,7 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[DEBUG] simple-builders: │ ├─ Creating JavaFileObject: test.VerboseTestBuilder", "[DEBUG] simple-builders: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/VerboseTestBuilder.java", "[DEBUG] simple-builders: │ ├─ Successfully generated class: VerboseTestBuilder", - "[DEBUG] simple-builders: │ └─ Generated builder with 1 fields and 10 methods for" + "[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: planning is skipped before the domain hook runs From ef06f397028b50c6eed2f1b7d918ceb2fba32341 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:42:07 +0000 Subject: [PATCH 159/170] Log why a round or element generates nothing --- docs/DEBUG_LOGGING.md | 1 + .../codegen/processing/AbstractCodegenProcessor.java | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md index 253ecff7..a3107595 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -189,6 +189,7 @@ Notes on individual lines: - `Writing class to file:` — the real path of the generated source file, resolved via the filer's `JavaFileObject.toUri()`. - `Registered for Jackson module in package ''` — emitted inside the class's generation scope by the Jackson module generator, only when the builder actually registered for module generation (i.e. `generateJacksonModule` enabled on that element's configuration and Jackson on the classpath). It is not a generation step of its own. - `Generated builder with N fields and M methods for ` — the domain's summary line, emitted as the last child (`├─`) inside the `Code generation for class` scope, which stays open until the post-generation step ran and then closes silently. +- A round or element that generates nothing logs why, for error analysis: `No matching elements collected — skipping planning for this round.`, `Processing is already finished — skipping planning for this round.`, `Planning produced no tasks — nothing to generate this round.`, or per element `Analysis produced no targets for `. - Code-generation phases with nothing to emit log nothing — a class without fields, methods or nested types (like `SimpleBuildersJacksonModule`, which only has a constructor and a mixin) produces no `Generating 0 …` / `added: 0` noise. **Note**: Debug messages are prefixed with `[DEBUG]` and use `Diagnostic.Kind.OTHER` which appears as `[INFO]` in Maven output. 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 7bcd8265..d2d57372 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -193,6 +193,9 @@ public final boolean process(Set annotations, RoundEnviro } finally { tracker.endPhase("Analysis"); } + if (definitions.isEmpty()) { + context.debug("Analysis produced no targets for " + task.element().getSimpleName()); + } for (D definition : definitions) { tracker.startPhase(); GenerationTargetClassDto target; @@ -473,7 +476,9 @@ private List> plan(ElementGroups collected, C context) { return List.of(); } List> tasks = planTasks(collected, context); - if (!tasks.isEmpty()) { + if (tasks.isEmpty()) { + context.debug("Planning produced no tasks — nothing to generate this round."); + } else { // 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; From 9a1d0059c496130ec95400592487113e38290fa1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:53:47 +0000 Subject: [PATCH 160/170] Remove unused private test helpers (java:S1144) --- .../logging/ActivePerformanceTrackerTest.java | 20 ------------------- 1 file changed, 20 deletions(-) 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 37000b68..3de92517 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,7 +31,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashSet; import java.util.Set; import org.javahelpers.simple.builders.processor.classgen.GenerationPhases; import org.javahelpers.simple.builders.processor.processing.PipelinePhases; @@ -75,15 +74,6 @@ private JsonNode generateReportAndParseJson(String outputFile) throws IOExceptio return new ObjectMapper().readTree(jsonContent); } - /** The {@code className} values of all class-metric entries. */ - private static Set classNames(JsonNode classMetrics) { - Set names = new HashSet<>(); - for (JsonNode cm : classMetrics) { - names.add(cm.get("className").asText()); - } - return names; - } - /** Asserts the metric entries are ordered by {@code elapsedNanos} descending. */ private static void assertSortedByElapsedDesc(JsonNode metrics) { for (int i = 1; i < metrics.size(); i++) { @@ -94,16 +84,6 @@ private static void assertSortedByElapsedDesc(JsonNode metrics) { } } - /** The class-metric entry for the given class name — fails the test when absent. */ - private static JsonNode classMetricByName(JsonNode root, String className) { - for (JsonNode cm : root.get("classMetrics")) { - if (className.equals(cm.get("className").asText())) { - return cm; - } - } - return fail("No classMetric entry for " + className); - } - /** * 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, From 583b010ff3724c73b6e98c48d4e128ab2cf1da6b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:53:47 +0000 Subject: [PATCH 161/170] Extract per-task specification to drop process() cognitive complexity (java:S3776) --- .../processing/AbstractCodegenProcessor.java | 58 ++++++++++++------- 1 file changed, 38 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 d2d57372..49db8bf3 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -186,26 +186,7 @@ public final boolean process(Set annotations, RoundEnviro tracker.startClass(task.element().getSimpleName().toString()); try { context.setCurrentTask(task); - tracker.startPhase(); - List definitions; - try { - definitions = analyse(task, context); - } finally { - tracker.endPhase("Analysis"); - } - if (definitions.isEmpty()) { - context.debug("Analysis produced no targets for " + task.element().getSimpleName()); - } - for (D definition : definitions) { - tracker.startPhase(); - GenerationTargetClassDto target; - try { - target = mapToTargetSpecification(definition, task.configuration(), context); - } finally { - tracker.endPhase("Target Mapping"); - } - planned.add(new PlannedGeneration<>(task, definition, target)); - } + planned.addAll(specify(task, context, tracker)); } 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. @@ -252,6 +233,43 @@ public final boolean process(Set annotations, RoundEnviro return claimsAnnotations(getSupportedAnnotationTypes()); } + /** + * Analyses one task and maps every resulting definition to its render target, tracking both + * phases on the class-level timer the caller started. Runs inside the task's debug scope. + * + * @param task the task to specify + * @param context the processing context + * @param tracker the round's performance tracker + * @return the planned generations of this task (possibly empty) + * @throws Exception forwarded from the domain's {@link #analyse}/{@link + * #mapToTargetSpecification} + */ + private List> specify( + CodegenTask task, C context, PerformanceTracker tracker) throws Exception { + tracker.startPhase(); + List definitions; + try { + definitions = analyse(task, context); + } finally { + tracker.endPhase("Analysis"); + } + if (definitions.isEmpty()) { + context.debug("Analysis produced no targets for " + task.element().getSimpleName()); + } + List> planned = new ArrayList<>(); + for (D definition : definitions) { + tracker.startPhase(); + GenerationTargetClassDto target; + try { + target = mapToTargetSpecification(definition, task.configuration(), context); + } finally { + tracker.endPhase("Target Mapping"); + } + planned.add(new PlannedGeneration<>(task, definition, target)); + } + return planned; + } + @Override public final Set getSupportedOptions() { return optionRegistry().supportedOptions(); From 9e91d2f02dad6af13d4490d32351ceb0689a6446 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:18:38 +0000 Subject: [PATCH 162/170] Use guard clauses to skip empty codegen phases --- .../roaster/RoasterCodeGenerator.java | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 263fcaa4..69568ea3 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -223,13 +223,15 @@ private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto c private void appendFields(JavaClassSource source, GenerationTargetClassDto classDef) { performanceTracker.startPhase(); - if (CollectionUtils.isNotEmpty(classDef.getClassFields())) { - logger.debugStartOperation("Generating %d fields", classDef.getClassFields().size()); - for (ClassFieldDto fieldDto : classDef.getClassFields()) { - appendField(source, fieldDto); - } - logger.debugEndOperation("Fields added: %d fields", source.getFields().size()); + if (CollectionUtils.isEmpty(classDef.getClassFields())) { + performanceTracker.endPhase(PHASE_FIELDS); + return; + } + logger.debugStartOperation("Generating %d fields", classDef.getClassFields().size()); + for (ClassFieldDto fieldDto : classDef.getClassFields()) { + appendField(source, fieldDto); } + logger.debugEndOperation("Fields added: %d fields", source.getFields().size()); performanceTracker.endPhase(PHASE_FIELDS); } @@ -244,13 +246,15 @@ private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { private void appendConstructors(JavaClassSource source, GenerationTargetClassDto classDef) { performanceTracker.startPhase(); - if (CollectionUtils.isNotEmpty(classDef.getConstructors())) { - logger.debugStartOperation("Generating %d constructors", classDef.getConstructors().size()); - for (ConstructorDto constructor : classDef.getConstructors()) { - appendConstructor(source, constructor); - } - logger.debugEndOperation("Constructors added: %d", classDef.getConstructors().size()); + if (CollectionUtils.isEmpty(classDef.getConstructors())) { + performanceTracker.endPhase(PHASE_CONSTRUCTORS); + return; + } + logger.debugStartOperation("Generating %d constructors", classDef.getConstructors().size()); + for (ConstructorDto constructor : classDef.getConstructors()) { + appendConstructor(source, constructor); } + logger.debugEndOperation("Constructors added: %d", classDef.getConstructors().size()); performanceTracker.endPhase(PHASE_CONSTRUCTORS); } @@ -268,19 +272,21 @@ private void appendConstructor(JavaClassSource source, ConstructorDto constructo private void appendMethods(JavaClassSource source, GenerationTargetClassDto classDef) { performanceTracker.startPhase(); - if (CollectionUtils.isNotEmpty(classDef.getMethods())) { - logger.debugStartOperation("Generating %d method candidates", classDef.getMethods().size()); + if (CollectionUtils.isEmpty(classDef.getMethods())) { + performanceTracker.endPhase(PHASE_METHODS); + return; + } + logger.debugStartOperation("Generating %d method candidates", classDef.getMethods().size()); - // Resolve method conflicts by signature and priority - List resolvedMethods = resolveMethodConflicts(classDef.getMethods()); - logger.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size()); + // Resolve method conflicts by signature and priority + List resolvedMethods = resolveMethodConflicts(classDef.getMethods()); + logger.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size()); - for (MethodDto methodDto : resolvedMethods) { - appendMethod(source, methodDto, false, false); - } - - logger.debugEndOperation("Methods added: %d", resolvedMethods.size()); + for (MethodDto methodDto : resolvedMethods) { + appendMethod(source, methodDto, false, false); } + + logger.debugEndOperation("Methods added: %d", resolvedMethods.size()); performanceTracker.endPhase(PHASE_METHODS); } From 0c518afdc1733bb9da65e9a1dacca76ab2452b2c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:18:38 +0000 Subject: [PATCH 163/170] Restore metric assert helpers and wire them into the JSON tests --- .../logging/ActivePerformanceTrackerTest.java | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) 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 3de92517..5620e299 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.HashSet; import java.util.Set; import org.javahelpers.simple.builders.processor.classgen.GenerationPhases; import org.javahelpers.simple.builders.processor.processing.PipelinePhases; @@ -84,6 +85,25 @@ private static void assertSortedByElapsedDesc(JsonNode metrics) { } } + /** Asserts the class-metric entries carry exactly the given class names. */ + private static void assertClassNames(JsonNode classMetrics, Set expected) { + Set names = new HashSet<>(); + for (JsonNode cm : classMetrics) { + names.add(cm.get("className").asText()); + } + assertEquals(expected, names); + } + + /** The class-metric entry for the given class name — fails the test when absent. */ + private static JsonNode classMetricByName(JsonNode root, String className) { + for (JsonNode cm : root.get("classMetrics")) { + if (className.equals(cm.get("className").asText())) { + return cm; + } + } + return fail("No classMetric entry for " + className); + } + /** * 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, @@ -321,14 +341,8 @@ void jsonReport_classMetrics_containsAllClassesSortedByElapsedDesc() throws IOEx JsonNode classMetrics = root.get("classMetrics"); assertEquals(2, classMetrics.size()); // Ordering is the contract (desc by elapsed); which class is first is timing-dependent. - assertEquals( - Set.of("TestClassA", "TestClassB"), - Set.of( - classMetrics.get(0).get("className").asText(), - classMetrics.get(1).get("className").asText())); - assertTrue( - classMetrics.get(0).get("elapsedNanos").asLong() - >= classMetrics.get(1).get("elapsedNanos").asLong()); + assertClassNames(classMetrics, Set.of("TestClassA", "TestClassB")); + assertSortedByElapsedDesc(classMetrics); } @Test @@ -336,13 +350,7 @@ void jsonReport_classMetrics_containsFieldAndCollectionCounts() throws IOExcepti Path jsonFile = tempDir.resolve("report.json"); JsonNode root = generateReportAndParseJson(jsonFile.toString()); - JsonNode classA = null; - for (JsonNode cm : root.get("classMetrics")) { - if ("TestClassA".equals(cm.get("className").asText())) { - classA = cm; - } - } - assertNotNull(classA); + JsonNode classA = classMetricByName(root, "TestClassA"); assertEquals(5, classA.get("fieldCount").asInt()); assertEquals(2, classA.get("collectionCount").asInt()); assertTrue(classA.has("elapsedMs")); From 8a7056397de9a4cff08ab4d5dc8520c80015e2aa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:18:38 +0000 Subject: [PATCH 164/170] Default reportingElement to the task element in build() --- .../java/org/javahelpers/codegen/processing/CodegenTask.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 13da4291..d7c6594c 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java @@ -67,9 +67,6 @@ public static final class Builder { /** The element the task generates for; also the default {@link #reportingElement}. */ public Builder element(Element element) { this.element = element; - if (this.reportingElement == null) { - this.reportingElement = element; - } return this; } @@ -96,7 +93,7 @@ public CodegenTask build() { return new CodegenTask<>( Objects.requireNonNull(element, "element"), Objects.requireNonNull(configuration, "configuration"), - Objects.requireNonNull(reportingElement, "reportingElement"), + reportingElement != null ? reportingElement : element, Objects.requireNonNull(outputPackage, "outputPackage")); } } From 5af51ed8a641a15fadddc06a2efd41629165983c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:18:56 +0000 Subject: [PATCH 165/170] Drop duplicate builder-name line before definition-extracted close --- docs/DEBUG_LOGGING.md | 1 - .../builders/processor/processing/BuilderDefinitionCreator.java | 2 -- .../simple/builders/processor/BuilderProcessorTest.java | 1 - 3 files changed, 4 deletions(-) diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md index a3107595..0a8d0e97 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -136,7 +136,6 @@ The following is a verbatim excerpt of a real run (`mvn -pl example clean compil [INFO] [DEBUG] simple-builders: │ │ │ ├─ Resolving method conflicts [INFO] [DEBUG] simple-builders: │ │ │ │ └─ Resolved: 11 signatures, 0 conflicts, 0 methods removed [INFO] [DEBUG] simple-builders: │ │ │ └─ Finalized: 1 class fields, 5 builder-level methods, 2 constructors -[INFO] [DEBUG] simple-builders: │ │ ├─ Builder will be generated as: SponsorDtoBuilder [INFO] [DEBUG] simple-builders: │ │ └─ Builder definition extracted: SponsorDtoBuilder … (the same extraction steps repeat for each remaining element) [INFO] [DEBUG] simple-builders: ├─ Generating targets for: SponsorDto diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index e88f114e..fdb12345 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -144,8 +144,6 @@ public static BuilderDefinitionDto extractFromElement( // Finalize the definition - convert to generic class representation finalizeDefinition(result, context); - context.debug("Builder will be generated as: %s", result.getBuilderTypeName().getClassName()); - context.debugEndOperation( "Builder definition extracted: %s", result.getBuilderTypeName().getClassName()); 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 81ecf68a..1ccbde38 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 @@ -139,7 +139,6 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[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", From 6a56de380317d8531598d018615623bdb8d5d437 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:18:56 +0000 Subject: [PATCH 166/170] Close the generation scope with the builder summary line --- docs/CONTRIBUTING.md | 2 +- docs/DEBUG_LOGGING.md | 4 ++-- .../processing/AbstractCodegenProcessor.java | 16 ++++++++-------- .../builders/processor/BuilderProcessor.java | 5 ++--- .../builders/processor/BuilderProcessorTest.java | 2 +- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index b143e010..6b8eed9c 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -273,7 +273,7 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md). [DEBUG] simple-builders: │ ├─ Creating JavaFileObject: test.ProjectBuilder [DEBUG] simple-builders: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/ProjectBuilder.java [DEBUG] simple-builders: │ ├─ Successfully generated class: ProjectBuilder -[DEBUG] simple-builders: │ ├─ Generated builder with 1 fields and 4 methods for ProjectBuilder +[DEBUG] simple-builders: │ └─ Generated builder with 1 fields and 4 methods for 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 0a8d0e97..e6044d72 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -156,7 +156,7 @@ The following is a verbatim excerpt of a real run (`mvn -pl example clean compil [INFO] [DEBUG] simple-builders: │ │ ├─ Creating JavaFileObject: org.javahelpers.simple.builders.example.SponsorDtoBuilder [INFO] [DEBUG] simple-builders: │ │ ├─ Writing class to file: …/example/generated-example-builder/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java [INFO] [DEBUG] simple-builders: │ │ ├─ Successfully generated class: SponsorDtoBuilder -[INFO] [DEBUG] simple-builders: │ │ ├─ Generated builder with 1 fields and 11 methods for SponsorDtoBuilder +[INFO] [DEBUG] simple-builders: │ │ └─ Generated builder with 1 fields and 11 methods for SponsorDtoBuilder … (the same code-generation steps repeat for each remaining element; elements whose configuration enables the Jackson module additionally emit a registration line inside the same generation scope:) @@ -187,7 +187,7 @@ Notes on individual lines: - `Applied: (priority: N): +M ` — logged **after** each generator/enhancer ran, with what it actually contributed (methods, constructors, nested types, annotations, interfaces, or javadoc). An enhancer that adds nothing measurable prints just `Applied: (priority: N)` with no suffix. - `Writing class to file:` — the real path of the generated source file, resolved via the filer's `JavaFileObject.toUri()`. - `Registered for Jackson module in package ''` — emitted inside the class's generation scope by the Jackson module generator, only when the builder actually registered for module generation (i.e. `generateJacksonModule` enabled on that element's configuration and Jackson on the classpath). It is not a generation step of its own. -- `Generated builder with N fields and M methods for ` — the domain's summary line, emitted as the last child (`├─`) inside the `Code generation for class` scope, which stays open until the post-generation step ran and then closes silently. +- `Generated builder with N fields and M methods for ` — the domain's summary line, emitted as the closing line (`└─`) of the `Code generation for class` scope, which stays open until the post-generation step ran. - A round or element that generates nothing logs why, for error analysis: `No matching elements collected — skipping planning for this round.`, `Processing is already finished — skipping planning for this round.`, `Planning produced no tasks — nothing to generate this round.`, or per element `Analysis produced no targets for `. - Code-generation phases with nothing to emit log nothing — a class without fields, methods or nested types (like `SimpleBuildersJacksonModule`, which only has a constructor and a mixin) produces no `Generating 0 …` / `added: 0` noise. 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 49db8bf3..a320d263 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -200,8 +200,8 @@ public final boolean process(Set annotations, RoundEnviro // Second loop: generation — every specification exists before any file is written. int successfulGenerations = 0; for (PlannedGeneration work : planned) { - // generateTarget leaves its generation-scope operation open so onTaskGenerated can emit - // its summary inside it — restore the pre-task indentation whatever the domain did. + // generateTarget leaves its generation-scope operation open so onTaskGenerated can close it + // with a summary line — restore the pre-task indentation whatever the domain did. int preTaskIndentation = logger.getCurrentIndentationLevel(); context.debugStartOperation( "Generating targets for: " + work.task().element().getSimpleName()); @@ -305,9 +305,9 @@ public SourceVersion getSupportedSourceVersion() { * already exist. Also usable by the domain's {@link #onProcessingOver} implementation for * last-round outputs. * - *

The engine leaves its generation-scope debug operation open so the caller can still emit - * inside it (see {@link #onTaskGenerated}); callers outside the task driver must close it with - * {@code context.debugEndOperation()}. + *

The engine leaves its generation-scope debug operation open so the caller can close it with + * a summary line (see {@link #onTaskGenerated}); callers outside the task driver must close it + * with {@code context.debugEndOperation()}. * * @param target the class definition to generate * @throws CodeGenerationException if the target exists or generation fails @@ -637,9 +637,9 @@ protected abstract GenerationTargetClassDto mapToTargetSpecification( * Event after a task's target generated successfully — the insert point for per-class metrics and * domain bookkeeping. Default: nothing. * - *

Runs while the generation scope is still open — emit the domain's summary with {@code - * context.debug(...)} as a child line (or {@code context.debugEndOperation(summary)} for a - * closing line); the driver closes whatever remains open. + *

Runs while the generation scope is still open — close it with {@code + * context.debugEndOperation(summary)} for a closing summary line, or leave it; the driver + * restores the pre-task indentation either way. * * @param generation the planned generation — task, analysed definition and generated target * @param context the processing 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 bd2ee4ce..d2c4c4f8 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 @@ -491,9 +491,8 @@ protected void onTaskGenerated( ctx.getPerformanceTracker() .endClass(definition.getAllFieldsForBuilder().size(), collectionCount); - // Summary of what was generated, inside the still-open generation scope — the driver - // closes the scope silently afterwards - ctx.debug( + // Close the generation scope with the summary of what was generated + ctx.debugEndOperation( "Generated builder with %d fields and %d methods for %s", definition.getAllFieldsForBuilder().size(), renderingDto.getMethods().size(), 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 1ccbde38..62e737df 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 @@ -161,7 +161,7 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[DEBUG] simple-builders: │ ├─ Creating JavaFileObject: test.VerboseTestBuilder", "[DEBUG] simple-builders: │ ├─ Writing class to file: /SOURCE_OUTPUT/test/VerboseTestBuilder.java", "[DEBUG] simple-builders: │ ├─ Successfully generated class: VerboseTestBuilder", - "[DEBUG] simple-builders: │ ├─ Generated builder with 1 fields and 10 methods for" + "[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: planning is skipped before the domain hook runs From 25bededa25d95bba97f4c99393a1ec68d359467d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:21:27 +0000 Subject: [PATCH 167/170] Log enhancer count also when none applied --- .../processor/generators/registry/GeneratorRegistry.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java index 6f2a2907..710a1021 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java @@ -162,11 +162,7 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, TypeName dtoType) { } } - if (appliedEnhancers > 0) { - context.debugEndOperation("Applied %d builder enhancers", appliedEnhancers); - } else { - context.debugEndOperation(); - } + context.debugEndOperation("Applied %d builder enhancers", appliedEnhancers); } /** Counts the content slots of a builder definition an enhancer can contribute to. */ From afc244e9360b30fc74b426945a1165fd30f62525 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:21:27 +0000 Subject: [PATCH 168/170] Default task configuration to CodegenConfiguration.DEFAULT --- .../java/org/javahelpers/codegen/processing/CodegenTask.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 d7c6594c..7891828d 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java @@ -58,9 +58,12 @@ public static Builder builder() { } /** Fluent builder for {@link CodegenTask}. */ + @SuppressWarnings("unchecked") public static final class Builder { private Element element; - private K configuration; + // Domains with a richer configuration type must set it explicitly — the default is the + // framework's plain CodegenConfiguration, which only fits K = CodegenConfiguration. + private K configuration = (K) CodegenConfiguration.DEFAULT; private Element reportingElement; private String outputPackage; From 6e8acf217052971e6be172271d9b27d83752d895 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:43:47 +0000 Subject: [PATCH 169/170] Default task configuration in build(), checked against the domain type --- .../codegen/processing/CodegenTask.java | 32 +++++++++++++------ .../MinimalCodegenProcessorTest.java | 3 +- 2 files changed, 24 insertions(+), 11 deletions(-) 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 7891828d..77bd78e8 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/CodegenTask.java @@ -48,25 +48,30 @@ public record CodegenTask( Element element, K configuration, Element reportingElement, String outputPackage) { /** - * A task builder — {@code reportingElement} defaults to the task's element when not set. + * A task builder — {@code reportingElement} defaults to the task's element and {@code + * configuration} to {@link CodegenConfiguration#DEFAULT} when that fits the given type. * * @param the configuration type + * @param configurationType the domain's configuration type; {@code build()} applies the {@link + * CodegenConfiguration#DEFAULT} only when it is assignable to it * @return a new builder */ - public static Builder builder() { - return new Builder<>(); + public static Builder builder(Class configurationType) { + return new Builder<>(Objects.requireNonNull(configurationType, "configurationType")); } /** Fluent builder for {@link CodegenTask}. */ - @SuppressWarnings("unchecked") public static final class Builder { + private final Class configurationType; private Element element; - // Domains with a richer configuration type must set it explicitly — the default is the - // framework's plain CodegenConfiguration, which only fits K = CodegenConfiguration. - private K configuration = (K) CodegenConfiguration.DEFAULT; + private K configuration; private Element reportingElement; private String outputPackage; + private Builder(Class configurationType) { + this.configurationType = configurationType; + } + /** The element the task generates for; also the default {@link #reportingElement}. */ public Builder element(Element element) { this.element = element; @@ -91,11 +96,20 @@ public Builder outputPackage(String outputPackage) { return this; } - /** Builds the task; the reporting element defaults to the task element. */ + /** + * Builds the task; the reporting element defaults to the task element and the configuration to + * {@link CodegenConfiguration#DEFAULT} — checked: the default is only applied when it is + * assignable to the domain's configuration type, otherwise {@code configuration()} is required. + */ public CodegenTask build() { + K resolvedConfiguration = configuration; + if (resolvedConfiguration == null + && configurationType.isInstance(CodegenConfiguration.DEFAULT)) { + resolvedConfiguration = configurationType.cast(CodegenConfiguration.DEFAULT); + } return new CodegenTask<>( Objects.requireNonNull(element, "element"), - Objects.requireNonNull(configuration, "configuration"), + Objects.requireNonNull(resolvedConfiguration, "configuration"), reportingElement != null ? reportingElement : element, Objects.requireNonNull(outputPackage, "outputPackage")); } diff --git a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java index 889b83ca..ff812e12 100644 --- a/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/codegen/processing/MinimalCodegenProcessorTest.java @@ -107,9 +107,8 @@ protected List> planTasks( return collected.all().stream() .map( element -> - CodegenTask.builder() + CodegenTask.builder(CodegenConfiguration.class) .element(element) - .configuration(CodegenConfiguration.DEFAULT) .outputPackage(ctx.getPackageName(element)) .build()) .toList(); From 36b97bb0031550f513979a862786bfce7ba4e914 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:26:26 +0000 Subject: [PATCH 170/170] =?UTF-8?q?processing:=20finish=20the=20round=20on?= =?UTF-8?q?=20an=20empty=20plan=20=E2=80=94=20out-of-scope=20beans=20must?= =?UTF-8?q?=20not=20keep=20MapStruct=20deferring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../processing/AbstractCodegenProcessor.java | 38 +++++++++++-------- .../MapStructSpiIntegrationTest.java | 19 ++++++++++ 2 files changed, 41 insertions(+), 16 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 a320d263..19253bd7 100644 --- a/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java +++ b/processor/src/main/java/org/javahelpers/codegen/processing/AbstractCodegenProcessor.java @@ -63,6 +63,7 @@ * ctx = createContext(env, reader, logger) [implement — in init] * onRoundStart(ctx) [default: no-op] * elements = findElements(annotations, roundEnv) [default: configuration's elementCollector] + * skipsRound(elements, ctx) [framework; empty or finished gate] * tasks = planTasks(elements, ctx) [implement] * onTasksPlanned(tasks, ctx) [default: no-op] * registerPlannedTaskResults(tasks, ctx) [framework; per-task names from predictTaskResultTypeNames() — default: empty] @@ -173,12 +174,13 @@ public final boolean process(Set annotations, RoundEnviro tracker.endPhase("Element Collection"); logger.debug("Processing round started."); - List> tasks = plan(collected, context); - onTasksPlanned(tasks, context); - if (tasks.isEmpty()) { + if (skipsRound(collected, context)) { return claimsAnnotations(getSupportedAnnotationTypes()); } + List> tasks = plan(collected, context); + onTasksPlanned(tasks, context); + // First loop: specification — analyse every task and map its render targets. List> planned = new ArrayList<>(); for (CodegenTask task : tasks) { @@ -475,24 +477,28 @@ protected ElementGroups findElements( return elementCollector().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(ElementGroups collected, C context) { + private boolean skipsRound(ElementGroups collected, C context) { if (collected.isEmpty()) { context.debug("No matching elements collected — skipping planning for this round."); - return List.of(); + return true; } if (isFinished(context)) { context.debug("Processing is already finished — skipping planning for this round."); - return List.of(); + return true; } + return false; + } + + /** + * The planning step — runs the domain's {@link #planTasks}, applies 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, possibly empty when there is nothing to generate + */ + private List> plan(ElementGroups collected, C context) { List> tasks = planTasks(collected, context); if (tasks.isEmpty()) { context.debug("Planning produced no tasks — nothing to generate this round."); @@ -659,7 +665,7 @@ protected void onProcessingOver(C context) { } /** - * Callback at the end of a generating round — domain finalization. + * Called at the end of every round that ran planning — also when the plan was empty. * * @param context the processing context */ diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/MapStructSpiIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/MapStructSpiIntegrationTest.java index 7be84fff..5295b17c 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/MapStructSpiIntegrationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/MapStructSpiIntegrationTest.java @@ -74,6 +74,25 @@ void mapStruct_processorOrderForward_shouldStillUseGeneratedBuilder() { assertTrue(mapperImpl.contains(".build()"), "MapStruct should finish via build()"); } + @Test + void mapStruct_markedBeanOutsideGenerationScope_shouldMapViaSetters() { + // A round whose plan is empty still finalizes the registry — MapStruct maps the out-of-scope + // bean without a builder instead of deferring until no implementation is created + Compilation compilation = + mapStructCompiler(new BuilderProcessor(), new MappingProcessor()) + .withOptions( + "-Amapstruct.suppressGeneratorTimestamp=true", + "-Amapstruct.suppressGeneratorVersionInfoComment=true", + "-Asimplebuilder.builderGenerationPackages=scoped") + .compile(mutableDto(), mutableDtoMapper()); + assertThat(compilation).succeeded(); + + String mapperImpl = loadGeneratedSource(compilation, "MutableDtoMapperImpl"); + assertFalse( + mapperImpl.contains("MutableDtoBuilder"), "An out-of-scope bean gets no builder to bind"); + assertTrue(mapperImpl.contains(".setName("), "MapStruct should fall back to setter mapping"); + } + @Test void mapStruct_shouldNotReportHelpersAsUnmappedTargetProperties() { Compilation compilation = mapStructCompiler().compile(personDto(), personDtoMapper());