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 16363245..0c560e70 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,11 +25,11 @@ package org.javahelpers.simple.builders.processor; import static org.javahelpers.simple.builders.processor.processing.BuilderDefinitionCreator.extractFromElement; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_BUILDER_DEFINITION_EXTRACTION; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_CODE_GENERATION; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_DTO_MAPPING; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_ELEMENT_COLLECTION; +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; @@ -63,6 +63,9 @@ 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.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator; @@ -79,6 +82,7 @@ import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; 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; @@ -95,25 +99,37 @@ public class BuilderProcessor extends AbstractProcessor { private ProcessingContext context; private ProcessingLogger logger; - private RoasterCodeGenerator codeGenerator; + private ClassCodeGenerator codeGenerator; + private SourceFormatterResolver formatterResolver; private JacksonModuleGenerator jacksonModuleGenerator; private boolean supportedJdk = true; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - this.logger = new ProcessingLogger(processingEnv); - logger.debug("Starting BuilderProcessor..."); - // Read global configuration from compiler arguments CompilerArgumentsReader reader = new CompilerArgumentsReader(processingEnv); - BuilderConfiguration globalConfig = reader.readBuilderConfiguration(logger); + this.logger = + reader.readBooleanValue(CompilerArgumentsEnum.VERBOSE) + ? ProcessingLogger.createVerbose(processingEnv) + : ProcessingLogger.create(processingEnv); + logger.debug("Starting BuilderProcessor..."); + + BuilderConfiguration globalConfig = + BuilderConfigurationReader.readCompilerConfiguration(reader, logger); logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig); SimpleBuildersSpiIntegration.initCompilation(processingEnv.getElementUtils()); this.context = new ProcessingContext(logger, globalConfig, processingEnv); - this.codeGenerator = new RoasterCodeGenerator(context, processingEnv); + this.codeGenerator = + new RoasterCodeGenerator( + new GenerationEnvironment( + processingEnv, + logger, + context.getPerformanceTracker(), + context::reportBasedOnStrictMode)); + this.formatterResolver = new SourceFormatterResolver(processingEnv, logger); this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig); // Initialize GeneratorRegistry once during processor initialization @@ -222,8 +238,10 @@ private void generateJacksonModules(PerformanceTracker tracker) { String packageName = moduleClassDef.getTypeName().getPackageName(); context.info("Generating Jackson Module in package '%s'", packageName); try { - codeGenerator.generateClass(moduleClassDef); - } catch (BuilderException e) { + 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( @@ -592,6 +610,38 @@ 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)); @@ -622,7 +672,13 @@ private void process(Element annotatedElement, BuilderConfiguration config, Stri // Track Code Generation (parent phase; sub-phases tracked inside RoasterCodeGenerator) tracker.startPhase(); - codeGenerator.generateClass(renderingDto); + 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 diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java index 22796f60..b83c7686 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java @@ -40,7 +40,6 @@ import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; -import org.javahelpers.simple.builders.processor.processing.ProcessingContext; /** Extractor for field annotations, converting them from Java model elements to DTOs. */ public final class FieldAnnotationExtractor { @@ -82,7 +81,7 @@ private FieldAnnotationExtractor() { * Extracts the {@code @Deprecated} annotation from the given element, preserving its members * (e.g. {@code since} and {@code forRemoval}). * - *
Unlike {@link #extractAnnotations(VariableElement, ProcessingContext)}, this method targets + *
Unlike {@link #extractAnnotations(VariableElement, TypeMappingContext)}, this method targets
* a single annotation and works on any {@link Element} (method, field, parameter, record
* component, type), not just {@link VariableElement}s.
*
@@ -92,7 +91,7 @@ private FieldAnnotationExtractor() {
* element is not deprecated
*/
public static Optional Mapping uses erasure to test whether a type is some kind of a known family (e.g. {@code
+ * Supplier}, {@code Collection}) independent of its type arguments.
+ *
+ * @param typeMirror the type mirror
+ * @return the erased type
+ */
+ TypeMirror erasure(TypeMirror typeMirror);
+
+ /**
+ * Tests whether one type is assignable to another.
+ *
+ * @param type1 the first type
+ * @param type2 the second type
+ * @return {@code true} if {@code type1} is assignable to {@code type2}
+ */
+ boolean isAssignable(TypeMirror type1, TypeMirror type2);
+
+ /**
+ * Tests whether two type mirrors represent the same type.
+ *
+ * @param type1 the first type
+ * @param type2 the second type
+ * @return {@code true} if both represent the same type
+ */
+ boolean isSameType(TypeMirror type1, TypeMirror type2);
+
+ /**
+ * Returns the direct supertypes of a type mirror.
+ *
+ * @param typeMirror the type mirror
+ * @return list of direct supertypes (wildcard enforced by the javax.lang.model API)
+ */
+ @SuppressWarnings("java:S1452")
+ List extends TypeMirror> directSupertypes(TypeMirror typeMirror);
+
+ /**
+ * Returns all members of a type, including inherited members.
+ *
+ * @param typeElement the type to inspect
+ * @return list of all member elements (wildcard enforced by the javax.lang.model API)
+ */
+ @SuppressWarnings("java:S1452")
+ List extends Element> getAllMembers(TypeElement typeElement);
+
+ /**
+ * Tests whether a member (type, method, constructor) is accessible from the package generated
+ * code is emitted into.
+ *
+ * @param member the member to check
+ * @return {@code true} if the member is accessible from the generated package
+ */
+ boolean isAccessibleFromGeneratedPackage(Element member);
+
+ /**
+ * Logs a debug message during type mapping when debug output is enabled.
+ *
+ * @param format the message format
+ * @param args the format arguments
+ */
+ void debug(String format, Object... args);
+
+ /**
+ * Whether annotations declared on a type usage are copied onto the mapped {@code TypeName}.
+ *
+ * @return {@code true} if type-use annotations are copied
+ */
+ boolean shouldCopyTypeAnnotations();
+
+ /**
+ * Resolves the generated type usable for the given type element (e.g. its builder).
+ *
+ * @param typeElement the type element to resolve
+ * @return the usable generated type, or empty when none applies
+ */
+ Optional Implementations are backend-specific (e.g. Roaster, JavaPoet) and are instantiated with a
+ * {@link GenerationEnvironment} providing the processing environment, logger, and performance
+ * tracker.
+ */
+public interface ClassCodeGenerator {
+
+ /**
+ * Generates a class from the given class definition and writes the source file.
+ *
+ * @param classDef DTO containing all information to create the class
+ * @param formatter formats the generated source before it is written
+ * @throws CodeGenerationException if there is an error in source code generation
+ */
+ void generateClass(GenerationTargetClassDto classDef, SourceFormatter formatter)
+ throws CodeGenerationException;
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/CodeGenerationException.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/CodeGenerationException.java
new file mode 100644
index 00000000..6e1aaf5e
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/CodeGenerationException.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+/**
+ * Failure of a {@link ClassCodeGenerator} to produce or write generated source code.
+ *
+ * The exception type of the code generation layer: it intentionally knows nothing about
+ * builders. Callers generating builders translate it into their domain exception.
+ */
+public class CodeGenerationException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Creates a code generation failure with a message.
+ *
+ * @param message the failure description
+ */
+ public CodeGenerationException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a code generation failure wrapping an underlying cause.
+ *
+ * @param cause the underlying failure
+ */
+ public CodeGenerationException(Throwable cause) {
+ super(cause.getMessage(), cause);
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java
new file mode 100644
index 00000000..d8753a45
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker;
+import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
+
+/**
+ * Environment handed to a {@link ClassCodeGenerator}: the annotation processing environment, the
+ * logger, the performance tracker and the reporter for generation problems.
+ */
+public class GenerationEnvironment {
+
+ private final ProcessingEnvironment processingEnvironment;
+ private final ProcessingLogger logger;
+ private final PerformanceTracker performanceTracker;
+ private final GenerationIssueReporter issueReporter;
+
+ /**
+ * Creates a new generation environment.
+ *
+ * @param processingEnvironment the annotation processing environment
+ * @param logger the logger for debug output during code generation
+ * @param performanceTracker the tracker for sub-phase timing
+ * @param issueReporter the reporter for problems found during generation; the reporter decides
+ * the severity
+ */
+ public GenerationEnvironment(
+ ProcessingEnvironment processingEnvironment,
+ ProcessingLogger logger,
+ PerformanceTracker performanceTracker,
+ GenerationIssueReporter issueReporter) {
+ this.processingEnvironment = processingEnvironment;
+ this.logger = logger;
+ this.performanceTracker = performanceTracker;
+ this.issueReporter = issueReporter;
+ }
+
+ /**
+ * Returns the annotation processing environment.
+ *
+ * @return the processing environment
+ */
+ public ProcessingEnvironment getProcessingEnvironment() {
+ return processingEnvironment;
+ }
+
+ /**
+ * Returns the logger for debug output during code generation.
+ *
+ * @return the logger
+ */
+ public ProcessingLogger getLogger() {
+ return logger;
+ }
+
+ /**
+ * Returns the performance tracker for sub-phase timing.
+ *
+ * @return the tracker
+ */
+ public PerformanceTracker getPerformanceTracker() {
+ return performanceTracker;
+ }
+
+ /**
+ * Returns the reporter for problems found during generation.
+ *
+ * @return the issue reporter
+ */
+ public GenerationIssueReporter getIssueReporter() {
+ return issueReporter;
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationIssueReporter.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationIssueReporter.java
new file mode 100644
index 00000000..ea2f947a
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationIssueReporter.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.simple.builders.processor.classgen;
+
+/**
+ * Reports a problem found during code generation. The receiving side decides the severity — e.g. an
+ * unexpected generated-code inconsistency may be a warning in normal operation and an error in a
+ * caller-defined strict mode.
+ */
+@FunctionalInterface
+public interface GenerationIssueReporter {
+
+ /**
+ * Reports a problem found during code generation.
+ *
+ * @param format the message format string
+ * @param args arguments referenced by the format specifiers in the format string
+ */
+ void report(String format, Object... args);
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java
new file mode 100644
index 00000000..0049f8f6
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java
@@ -0,0 +1,52 @@
+/*
+ * 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;
+
+/**
+ * Phase names a {@link ClassCodeGenerator} may report through {@code PerformanceTracker.endPhase}
+ * while turning a class definition into a source file.
+ */
+public final class GenerationPhases {
+
+ // Code Generation children
+ public static final String PHASE_SOURCE_CONSTRUCTION = "Source Construction";
+ public static final String PHASE_FILE_WRITING = "File Writing";
+
+ // Source Construction children
+ public static final String PHASE_ELEMENT_BUILDING = "Element Building";
+ public static final String PHASE_STRING_GENERATION = "String Generation";
+ public static final String PHASE_FORMATTING = "Formatting";
+
+ // Element Building children
+ public static final String PHASE_CLASS_CREATION = "Class Creation";
+ public static final String PHASE_CLASS_METADATA = "Class Metadata";
+ public static final String PHASE_FIELDS = "Fields";
+ public static final String PHASE_CONSTRUCTORS = "Constructors";
+ public static final String PHASE_METHODS = "Methods";
+ public static final String PHASE_NESTED_TYPES = "Nested Types";
+ public static final String PHASE_CLASS_ANNOTATIONS = "Class Annotations";
+
+ private GenerationPhases() {}
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/SourceFormatter.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/SourceFormatter.java
new file mode 100644
index 00000000..b4cf0fa2
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/SourceFormatter.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+/**
+ * Formats generated source code before it is written to disk.
+ *
+ * The generic formatting seam of the code generation layer: callers pick the formatter
+ * implementation matching the target's formatting mode and pass it to {@link
+ * ClassCodeGenerator#generateClass}. Formatting-heavy backends may implement richer formatters
+ * behind this interface.
+ */
+@FunctionalInterface
+public interface SourceFormatter {
+
+ /**
+ * Formats generated source code.
+ *
+ * @param rawSource the unformatted source code
+ * @return the formatted source code
+ */
+ String format(String rawSource);
+}
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 35a552cd..c8763693 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
@@ -24,15 +24,14 @@
package org.javahelpers.simple.builders.processor.classgen.roaster;
+import static org.javahelpers.simple.builders.processor.classgen.GenerationPhases.*;
import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapType;
import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate;
-import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.*;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -40,13 +39,15 @@
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.processing.ProcessingEnvironment;
-import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import org.apache.commons.collections4.CollectionUtils;
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.exceptions.BuilderException;
+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.GenerationIssueReporter;
+import org.javahelpers.simple.builders.processor.classgen.SourceFormatter;
import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto;
import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName;
import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto;
@@ -62,7 +63,8 @@
import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.model.type.TypeNameArray;
-import org.javahelpers.simple.builders.processor.processing.ProcessingContext;
+import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker;
+import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
import org.javahelpers.simple.builders.processor.util.ImportCollector;
import org.jboss.forge.roaster.Roaster;
import org.jboss.forge.roaster.model.source.AnnotationSource;
@@ -75,84 +77,82 @@
import org.jboss.forge.roaster.model.source.TypeVariableSource;
/** Roaster-based code generator for builder source files. */
-public class RoasterCodeGenerator {
- /** Processing context providing logger, performance tracking, type lookup and profile. */
- private final ProcessingContext context;
-
- /** Processing environment for creating generated source files via its filer. */
+public class RoasterCodeGenerator implements ClassCodeGenerator {
+ /** Processing environment for accessing filer and element utilities. */
private final ProcessingEnvironment processingEnv;
- /** Cached formatters per formatting mode (at most 3 instances, created lazily). */
- private final EnumMap At most 3 formatter instances exist (one per {@link FormattingMode} enum value), created
- * lazily on first use.
+ * Creates a code generator using the given generation environment.
*
- * @param mode the formatting mode
- * @return a cached or new formatter instance
+ * @param environment the generation environment providing processing environment, logger,
+ * performance tracker and issue reporter
*/
- private RoasterSourceFormatter getFormatter(FormattingMode mode) {
- return formatterCache.computeIfAbsent(mode, context::createSourceFormatter);
+ public RoasterCodeGenerator(GenerationEnvironment environment) {
+ this.processingEnv =
+ Objects.requireNonNull(
+ environment.getProcessingEnvironment(), "processingEnv must not be null");
+ this.logger = Objects.requireNonNull(environment.getLogger(), "logger must not be null");
+ this.performanceTracker =
+ Objects.requireNonNull(
+ environment.getPerformanceTracker(), "performanceTracker must not be null");
+ this.issueReporter =
+ Objects.requireNonNull(environment.getIssueReporter(), "issueReporter must not be null");
}
/**
* Generates a class from the given class definition.
*
* @param classDef DTO containing all information to create the class
- * @throws BuilderException if there is an error in source code generation
+ * @param formatter formats the generated source before it is written
+ * @throws CodeGenerationException if there is an error in source code generation
*/
- public void generateClass(GenerationTargetClassDto classDef) throws BuilderException {
- context.debugStartOperation(
+ @Override
+ public void generateClass(GenerationTargetClassDto classDef, SourceFormatter formatter)
+ throws CodeGenerationException {
+ logger.debugStartOperation(
"Code generation for class: %s", classDef.getTypeName().getClassName());
String sourceCode;
try {
- context.startPerformancePhase();
+ performanceTracker.startPhase();
JavaClassSource source = buildClassSource(classDef);
- context.startPerformancePhase();
+ performanceTracker.startPhase();
String unformatted = source.toUnformattedString();
- context.endPerformancePhase(PHASE_STRING_GENERATION);
- context.startPerformancePhase();
- sourceCode = formatSource(unformatted, classDef.getFormattingMode());
+ performanceTracker.endPhase(PHASE_STRING_GENERATION);
+ performanceTracker.startPhase();
+ sourceCode = formatSource(unformatted, formatter);
// Roaster renders some java.lang annotations (e.g. @SuppressWarnings, @Deprecated with
// members) with their FQN (@java.lang.SuppressWarnings) even though java.lang types don't
// need qualification. Fix this by replacing @java.lang.Xxx with @Xxx for known annotations.
sourceCode = sourceCode.replace("@java.lang.SuppressWarnings", "@SuppressWarnings");
sourceCode = sourceCode.replace("@java.lang.Deprecated", "@Deprecated");
- context.endPerformancePhase(PHASE_FORMATTING);
- context.endPerformancePhase(PHASE_SOURCE_CONSTRUCTION);
+ performanceTracker.endPhase(PHASE_FORMATTING);
+ performanceTracker.endPhase(PHASE_SOURCE_CONSTRUCTION);
} catch (RuntimeException ex) {
- // Rendering failures (e.g. RoasterMapperException) are RuntimeExceptions. Convert them into
- // a BuilderException so callers can isolate the failure to this single class and keep
- // generating the remaining builders instead of aborting the whole processing round.
- throw new BuilderException(null, ex);
+ // Rendering failures (e.g. RoasterMapperException) are RuntimeExceptions. Convert them
+ // into a CodeGenerationException so callers can isolate the failure to this single class and
+ // keep generating the remaining classes instead of aborting the whole processing round.
+ throw new CodeGenerationException(ex);
}
- context.startPerformancePhase();
+ performanceTracker.startPhase();
writeClassToFile(sourceCode, classDef);
- context.endPerformancePhase(PHASE_FILE_WRITING);
+ performanceTracker.endPhase(PHASE_FILE_WRITING);
- context.debugEndOperation(
+ logger.debugEndOperation(
"Successfully generated class: %s", classDef.getTypeName().getClassName());
}
private JavaClassSource buildClassSource(GenerationTargetClassDto classDef) {
- context.startPerformancePhase();
+ performanceTracker.startPhase();
JavaClassSource source = createJavaClassSource(classDef);
addClassMetadata(source, classDef);
appendFields(source, classDef);
@@ -160,24 +160,24 @@ private JavaClassSource buildClassSource(GenerationTargetClassDto classDef) {
appendMethods(source, classDef);
appendNestedTypes(source, classDef);
applyClassAnnotations(source, classDef);
- context.endPerformancePhase(PHASE_ELEMENT_BUILDING);
+ performanceTracker.endPhase(PHASE_ELEMENT_BUILDING);
return source;
}
private void applyClassAnnotations(JavaClassSource source, GenerationTargetClassDto classDef) {
- context.startPerformancePhase();
+ performanceTracker.startPhase();
if (CollectionUtils.isNotEmpty(classDef.getClassAnnotations())) {
// Adding class annotations
applyAnnotations(source, classDef.getClassAnnotations());
- context.debug("Class-level annotations added");
+ logger.debug("Class-level annotations added");
}
- context.endPerformancePhase(PHASE_CLASS_ANNOTATIONS);
+ performanceTracker.endPhase(PHASE_CLASS_ANNOTATIONS);
}
private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) {
- context.startPerformancePhase();
+ performanceTracker.startPhase();
if (CollectionUtils.isNotEmpty(classDef.getGenerics())) {
- context.debug("Class has %d generic type parameter(s)", classDef.getGenerics().size());
+ logger.debug("Class has %d generic type parameter(s)", classDef.getGenerics().size());
}
JavaClassSource source = Roaster.create(JavaClassSource.class);
@@ -200,13 +200,13 @@ private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef)
}
}
- context.debug("JavaClassSource created");
- context.endPerformancePhase(PHASE_CLASS_CREATION);
+ logger.debug("JavaClassSource created");
+ performanceTracker.endPhase(PHASE_CLASS_CREATION);
return source;
}
private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto classDef) {
- context.startPerformancePhase();
+ performanceTracker.startPhase();
applyJavadoc(source, classDef.getClassJavadoc());
applyVisibility(source, classDef.getClassAccessModifier());
applySuperType(source, classDef.getSuperType());
@@ -216,20 +216,20 @@ private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto c
source.addInterface(RoasterMapper.mapInterfaceToTypeName(interfaceName));
}
- context.debug("Class metadata added");
- context.endPerformancePhase(PHASE_CLASS_METADATA);
+ logger.debug("Class metadata added");
+ performanceTracker.endPhase(PHASE_CLASS_METADATA);
}
private void appendFields(JavaClassSource source, GenerationTargetClassDto classDef) {
- context.startPerformancePhase();
- context.debugStartOperation("Generating %d fields", classDef.getClassFields().size());
+ performanceTracker.startPhase();
+ logger.debugStartOperation("Generating %d fields", classDef.getClassFields().size());
for (ClassFieldDto fieldDto : classDef.getClassFields()) {
appendField(source, fieldDto);
}
- context.debugEndOperation("Fields added: %d fields", source.getFields().size());
- context.endPerformancePhase(PHASE_FIELDS);
+ logger.debugEndOperation("Fields added: %d fields", source.getFields().size());
+ performanceTracker.endPhase(PHASE_FIELDS);
}
private void appendField(JavaClassSource source, ClassFieldDto fieldDto) {
@@ -242,15 +242,15 @@ private void appendField(JavaClassSource source, ClassFieldDto fieldDto) {
}
private void appendConstructors(JavaClassSource source, GenerationTargetClassDto classDef) {
- context.startPerformancePhase();
- context.debugStartOperation("Generating %d constructors", classDef.getConstructors().size());
+ performanceTracker.startPhase();
+ logger.debugStartOperation("Generating %d constructors", classDef.getConstructors().size());
for (ConstructorDto constructor : classDef.getConstructors()) {
appendConstructor(source, constructor);
}
- context.debugEndOperation("Constructors added: %d", classDef.getConstructors().size());
- context.endPerformancePhase(PHASE_CONSTRUCTORS);
+ logger.debugEndOperation("Constructors added: %d", classDef.getConstructors().size());
+ performanceTracker.endPhase(PHASE_CONSTRUCTORS);
}
private void appendConstructor(JavaClassSource source, ConstructorDto constructor) {
@@ -266,19 +266,19 @@ private void appendConstructor(JavaClassSource source, ConstructorDto constructo
}
private void appendMethods(JavaClassSource source, GenerationTargetClassDto classDef) {
- context.startPerformancePhase();
- context.debugStartOperation("Generating %d method candidates", classDef.getMethods().size());
+ performanceTracker.startPhase();
+ logger.debugStartOperation("Generating %d method candidates", classDef.getMethods().size());
// Resolve method conflicts by signature and priority
List The Eclipse formatter profile can be overridden with a file system path or classpath resource.
*/
-public final class RoasterSourceFormatter {
+public final class RoasterSourceFormatter implements SourceFormatter {
static final String DEFAULT_FORMATTER_PROFILE_RESOURCE = "eclipse-java-format.xml";
private static final int SPACES_PER_TAB = 2;
private final ProcessingLogger logger;
private final FormattingMode formattingMode;
- private final Properties formatterProperties;
private final String formatterProfileResource;
+ private Properties formatterProperties;
/**
* Creates a formatter instance.
@@ -86,7 +87,13 @@ public RoasterSourceFormatter(
this.formattingMode = Objects.requireNonNull(formattingMode, "formattingMode must not be null");
this.formatterProfileResource =
StringUtils.defaultIfBlank(formatterProfile, DEFAULT_FORMATTER_PROFILE_RESOURCE);
- this.formatterProperties = initializeFormatterProperties(formattingMode);
+ }
+
+ private Properties formatterProperties() {
+ if (formatterProperties == null) {
+ formatterProperties = initializeFormatterProperties(formattingMode);
+ }
+ return formatterProperties;
}
/**
@@ -126,17 +133,19 @@ private Properties loadJdtFormatterProperties() {
* @param rawSource the unformatted Java source code from Roaster's {@code toUnformattedString()}
* @return the formatted source code
*/
+ @Override
public String format(String rawSource) {
+ Properties properties = formatterProperties();
if (formattingMode == FormattingMode.NONE) {
return rawSource;
}
if (formattingMode == FormattingMode.LIGHTWEIGHT) {
return lightweightFormat(rawSource);
}
- if (formatterProperties.isEmpty()) {
+ if (properties.isEmpty()) {
return lightweightFormat(rawSource);
}
- return Roaster.format(formatterProperties, rawSource);
+ return Roaster.format(properties, rawSource);
}
/**
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java
index c989db64..18fddb94 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java
@@ -39,8 +39,8 @@
* Exception classes are organized in separate packages:
*
* This method reads all configuration options from compiler arguments like:
+ *
+ * Options set via {@code @SimpleBuilder.Options} on the annotated type are not handled here;
+ * they are read by {@link BuilderConfigurationReader} and merged on top of this global
+ * configuration.
+ *
+ * All values default to UNSET or DEFAULT if not specified in compiler arguments.
+ *
+ * Adding a new option: every {@link CompilerArgumentsEnum} constant is applied; those
+ * without a builder applier are no-ops in {@link CompilerArgumentsEnum#apply}, so wiring a new
+ * option means declaring the applier on the enum constant once — no change is needed here. The
+ * remaining checklist when adding a new option:
+ *
+ * Each enum constant provides the option name and the full compiler argument.
*/
-public enum CompilerArgumentsEnum {
+public enum CompilerArgumentsEnum implements CompilerOption {
// === Field Setter Generation ===
/** Option for field supplier generation. */
GENERATE_FIELD_SUPPLIER("generateFieldSupplier", optionState(Builder::generateSupplier)),
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 5f4ee76d..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
@@ -25,18 +25,14 @@
package org.javahelpers.simple.builders.processor.processing;
import java.util.Map;
-import java.util.stream.Stream;
import javax.annotation.processing.ProcessingEnvironment;
import org.apache.commons.lang3.Strings;
-import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
-import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
/**
* Utility class for reading compiler arguments from the annotation processing environment.
*
- * This class provides a centralized way to read compiler arguments using {@link
- * CompilerArgumentsEnum} values, ensuring consistent handling of option names and values across the
- * processor.
+ * This class provides a centralized way to read compiler arguments using {@link CompilerOption}
+ * values, ensuring consistent handling of option names and values across the processor.
*/
public class CompilerArgumentsReader {
private final ProcessingEnvironment processingEnv;
@@ -59,10 +55,10 @@ public CompilerArgumentsReader(ProcessingEnvironment processingEnv) {
* property is available when the build tool runs javac in-process and is not available with
* {@code This method reads all configuration options from compiler arguments like:
- *
- * Options set via {@code @SimpleBuilder.Options} on the annotated type are not handled here;
- * they are read by {@link BuilderConfigurationReader} and merged on top of this global
- * configuration.
- *
- * All values default to UNSET or DEFAULT if not specified in compiler arguments.
- *
- * Adding a new option: every {@link CompilerArgumentsEnum} constant is applied; those
- * without a builder applier are no-ops in {@link CompilerArgumentsEnum#apply}, so wiring a new
- * option means declaring the applier on the enum constant once — no change is needed here or in
- * {@code BuilderConfigurationReader}. The remaining checklist when adding a new option:
- *
- * An option has a bare option name (e.g. {@code "verbose"}) and a prefixed compiler argument
+ * name (e.g. {@code "simplebuilder.verbose"}) which is looked up as {@code -A} compiler argument
+ * and as {@code -D} JVM system property.
+ */
+public interface CompilerOption {
+
+ /**
+ * Returns the bare option name (e.g. {@code "verbose"}).
+ *
+ * @return the option name without prefix
+ */
+ String getOptionName();
+
+ /**
+ * Returns the prefixed compiler argument name (e.g. {@code "simplebuilder.verbose"}).
+ *
+ * @return the compiler argument name
+ */
+ String getCompilerArgument();
+}
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
new file mode 100644
index 00000000..f045feea
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/PipelinePhases.java
@@ -0,0 +1,84 @@
+/*
+ * 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 static org.javahelpers.simple.builders.processor.classgen.GenerationPhases.*;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.javahelpers.simple.builders.processor.processing.logging.PhaseHierarchy;
+
+/**
+ * Phase names of the builder processing pipeline, reported through {@code
+ * PerformanceTracker.endPhase}, plus the phase hierarchy an {@code ActivePerformanceTracker} needs
+ * to render its report.
+ */
+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_CODE_GENERATION = "Code Generation";
+
+ /** Top-level phases of the pipeline, in report display order. */
+ private static final List This eliminates repetitive parameter passing and provides a cleaner API.
*/
-public final class ProcessingContext {
+public final class ProcessingContext implements TypeMappingContext {
private final Elements elementUtils;
private final Types typeUtils;
private final ProcessingLogger logger;
private final BuilderConfigurationReader configurationReader;
private final ProcessingEnvironment processingEnv;
private final PerformanceTracker performanceTracker;
- private final String formatterProfile;
private final BuilderScopeResolver builderScopeResolver;
+
private GeneratorRegistry generatorRegistry;
private ProcessingTarget processingTarget;
@@ -90,11 +90,9 @@ public ProcessingContext(
boolean perfTrackingEnabled =
argReader.readBooleanValue(CompilerArgumentsEnum.PERFORMANCE_TRACKING);
String perfOutputFile = argReader.readValue(CompilerArgumentsEnum.PERFORMANCE_OUTPUT_FILE);
- this.formatterProfile =
- StringUtils.trimToNull(argReader.readValue(CompilerArgumentsEnum.FORMATTER_PROFILE));
this.performanceTracker =
perfTrackingEnabled
- ? new ActivePerformanceTracker(perfOutputFile)
+ ? new ActivePerformanceTracker(perfOutputFile, PipelinePhases.PIPELINE_HIERARCHY)
: new NoOpPerformanceTracker();
this.builderScopeResolver = new BuilderScopeResolver(this);
// GeneratorRegistry will be lazily initialized on first access
@@ -140,6 +138,17 @@ public String getBuilderPackageName() {
* @param member the member to check
* @return {@code true} if generated code in the builder package may call the member
*/
+ @Override
+ public boolean isAccessibleFromGeneratedPackage(Element member) {
+ return isMemberAccessibleFromBuilderPackage(member);
+ }
+
+ /**
+ * Tests whether a member is accessible from the package the generated builder is written to.
+ *
+ * @param member the member to check
+ * @return {@code true} if the member is accessible from the builder package
+ */
public boolean isMemberAccessibleFromBuilderPackage(Element member) {
if (member.getModifiers().contains(Modifier.PUBLIC)) {
return true;
@@ -217,17 +226,6 @@ public void endPerformancePhase(String phase) {
performanceTracker.endPhase(phase);
}
- /**
- * Creates a {@link RoasterSourceFormatter} for the given formatting mode, applying the configured
- * Eclipse formatter profile when one is set.
- *
- * @param mode the formatting mode
- * @return a new formatter instance
- */
- public RoasterSourceFormatter createSourceFormatter(FormattingMode mode) {
- return new RoasterSourceFormatter(logger, mode, formatterProfile);
- }
-
/**
* Get the builder scope resolver for deciding whether a builder may be referenced.
*
@@ -240,12 +238,33 @@ public BuilderScopeResolver getBuilderScopeResolver() {
return builderScopeResolver;
}
+ /**
+ * {@inheritDoc}
+ *
+ * Backed by the resolved per-target builder configuration.
+ */
+ @Override
+ public boolean shouldCopyTypeAnnotations() {
+ return getConfiguration().shouldCopyTypeAnnotations();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Delegated to the builder scope resolver.
+ */
+ @Override
+ public Optional At most one formatter instance per {@link FormattingMode} is created, lazily on first resolve.
+ */
+public class SourceFormatterResolver {
+
+ private final ProcessingLogger logger;
+ private final String formatterProfile;
+ private final EnumMap The tracker maintains per-phase totals (with a hardcoded hierarchy defined by {@link
- * #PHASE_CHILDREN} for report display), per-generator and per-enhancer totals with call counts, and
- * per-class metrics including field and collection counts.
+ * The tracker maintains per-phase totals (rendered using the phase hierarchy handed in at
+ * construction), per-generator and per-enhancer totals with call counts, and per-class metrics
+ * including field and collection counts.
*
* When an output file path is provided, {@link #generateReport(ProcessingLogger)} writes a
* structured JSON report with hierarchical phase breakdown, class metrics, and generator/enhancer
@@ -64,38 +64,14 @@
*/
public final class ActivePerformanceTracker implements PerformanceTracker {
- /** Hardcoded phase hierarchy for report display. Order defines display order. */
- private static final List The phase names themselves are consumer-defined — this type carries whichever pipeline
+ * vocabulary the caller tracks with, keeping the tracker independent of any concrete phase set.
+ *
+ * @param topLevelPhases top-level phases in display order
+ * @param subPhases parent phase name to its ordered sub-phase names
+ */
+public record PhaseHierarchy(List Rendering failures surface as {@link RoasterMapperException}, which extends {@link
- * RuntimeException}. These must be converted into the checked {@link BuilderException} so that the
- * per-builder resilience loops in {@code BuilderProcessor} can isolate the failure to the offending
- * class and continue generating the remaining builders instead of aborting the whole round.
+ * RuntimeException}. These must be converted into the checked {@link CodeGenerationException} so
+ * that the per-builder resilience loops in {@code BuilderProcessor} can isolate the failure to the
+ * offending class and continue generating the remaining builders instead of aborting the whole
+ * round.
*/
class RoasterCodeGeneratorResilienceTest {
@@ -65,7 +69,7 @@ private UnsupportedPlaceholder(String label, String value) {
}
@Test
- void shouldWrapRenderingRuntimeExceptionInBuilderException() {
+ void shouldWrapRenderingRuntimeExceptionInCodeGenerationException() {
GenerationTargetClassDto classDef = new GenerationTargetClassDto();
classDef.setTypeName(new TypeName("com.example", "FailingBuilder"));
classDef.setClassAccessModifier(AccessModifier.PUBLIC);
@@ -80,12 +84,18 @@ void shouldWrapRenderingRuntimeExceptionInBuilderException() {
classDef.addConstructor(constructor);
ProcessingEnvironment env = ProcessingEnvironmentStub.createEmpty();
- ProcessingLogger logger = new ProcessingLogger(env);
+ ProcessingLogger logger = ProcessingLogger.create(env);
ProcessingContext context = new ProcessingContext(logger, BuilderConfiguration.DEFAULT, env);
- RoasterCodeGenerator generator = new RoasterCodeGenerator(context, env);
+ RoasterCodeGenerator generator =
+ new RoasterCodeGenerator(
+ new GenerationEnvironment(
+ env, logger, context.getPerformanceTracker(), context::reportBasedOnStrictMode));
- BuilderException thrown =
- assertThrows(BuilderException.class, () -> generator.generateClass(classDef));
+ SourceFormatter formatter =
+ new SourceFormatterResolver(env, logger).resolve(FormattingMode.NONE);
+ CodeGenerationException thrown =
+ assertThrows(
+ CodeGenerationException.class, () -> generator.generateClass(classDef, formatter));
assertInstanceOf(RoasterMapperException.class, thrown.getCause());
}
@@ -109,10 +119,18 @@ void shouldKeepFirstMethodAndWarnOnDuplicateSignature() {
ProcessingEnvironment env = ProcessingEnvironmentStub.createEmpty();
ProcessingContext context =
new ProcessingContext(capturing.logger(), BuilderConfiguration.DEFAULT, env);
- RoasterCodeGenerator generator = new RoasterCodeGenerator(context, env);
+ RoasterCodeGenerator generator =
+ new RoasterCodeGenerator(
+ new GenerationEnvironment(
+ env,
+ capturing.logger(),
+ context.getPerformanceTracker(),
+ context::reportBasedOnStrictMode));
+ SourceFormatter formatter =
+ new SourceFormatterResolver(env, capturing.logger()).resolve(FormattingMode.NONE);
// generation itself succeeds; only the file-write step fails on the stub's null filer
- assertThrows(NullPointerException.class, () -> generator.generateClass(classDef));
+ assertThrows(NullPointerException.class, () -> generator.generateClass(classDef, formatter));
capturing.assertMessage(
"WARNING: Unexpected duplicate method signature: 'name(java.lang.String)'"
+ " — keeping first occurrence (safety net)");
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatterTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatterTest.java
index 14117481..0c05487d 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatterTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatterTest.java
@@ -55,6 +55,9 @@
*/
class RoasterSourceFormatterTest {
+ /** Minimal compilable input used when a test only needs to trigger profile initialization. */
+ private static final String TESTDATA_CODE = "class A {}";
+
private RoasterSourceFormatter createFormatter(FormattingMode mode) {
return new RoasterSourceFormatter(CapturingProcessingLogger.create().logger(), mode);
}
@@ -560,14 +563,16 @@ void format_jdtMode_withProfile_producesFormattedOutput() {
void constructor_lightweightMode_missingProfile_noFallbackWarning() {
CapturingProcessingLogger capturing = CapturingProcessingLogger.createDebugEnabled();
new RoasterSourceFormatter(
- capturing.logger(), FormattingMode.LIGHTWEIGHT, "nonexistent-profile.xml");
+ capturing.logger(), FormattingMode.LIGHTWEIGHT, "nonexistent-profile.xml")
+ .format(TESTDATA_CODE);
capturing.assertMessage("OTHER: [DEBUG] Using lightweight source formatting.");
}
@Test
void constructor_missingProfile_logsProfileNotFoundWarning() {
CapturingProcessingLogger capturing = CapturingProcessingLogger.create();
- new RoasterSourceFormatter(capturing.logger(), FormattingMode.JDT, "nonexistent-profile.xml");
+ new RoasterSourceFormatter(capturing.logger(), FormattingMode.JDT, "nonexistent-profile.xml")
+ .format(TESTDATA_CODE);
capturing.assertMessage(
"WARNING: Eclipse formatter profile 'nonexistent-profile.xml' was not found as a file or classpath resource; falling back to the bundled profile.");
}
@@ -607,9 +612,10 @@ void constructor_fileSystemProfile_isUsedForFormatting(@TempDir Path tempDir) th
void constructor_classpathProfile_explicitDefaultName_noWarning() {
CapturingProcessingLogger capturing = CapturingProcessingLogger.createDebugEnabled();
new RoasterSourceFormatter(
- capturing.logger(),
- FormattingMode.JDT,
- RoasterSourceFormatter.DEFAULT_FORMATTER_PROFILE_RESOURCE);
+ capturing.logger(),
+ FormattingMode.JDT,
+ RoasterSourceFormatter.DEFAULT_FORMATTER_PROFILE_RESOURCE)
+ .format(TESTDATA_CODE);
capturing.assertMessage(
"OTHER: [DEBUG] Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'.");
}
@@ -636,7 +642,7 @@ void constructor_missingProfile_fallsBackToBundledProfile() {
@Test
void constructor_blankProfile_usesBundledDefault() {
CapturingProcessingLogger capturing = CapturingProcessingLogger.createDebugEnabled();
- new RoasterSourceFormatter(capturing.logger(), FormattingMode.JDT, " ");
+ new RoasterSourceFormatter(capturing.logger(), FormattingMode.JDT, " ").format(TESTDATA_CODE);
capturing.assertMessage(
"OTHER: [DEBUG] Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'.");
}
@@ -645,7 +651,8 @@ void constructor_blankProfile_usesBundledDefault() {
void constructor_malformedProfile_logsLoadFailureWarning() {
CapturingProcessingLogger capturing = CapturingProcessingLogger.createDebugEnabled();
new RoasterSourceFormatter(
- capturing.logger(), FormattingMode.JDT, "eclipse-java-format-malformed.xml");
+ capturing.logger(), FormattingMode.JDT, "eclipse-java-format-malformed.xml")
+ .format(TESTDATA_CODE);
capturing.assertMessageMatching(
w ->
w.startsWith(
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 a8b07629..c8ea663b 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,8 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import org.javahelpers.simple.builders.processor.classgen.GenerationPhases;
+import org.javahelpers.simple.builders.processor.processing.PipelinePhases;
import org.javahelpers.simple.builders.processor.testing.CapturingProcessingLogger;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -47,10 +49,11 @@ class ActivePerformanceTrackerTest {
/** Helper to create a tracker, track some data, and generate report with JSON output. */
private JsonNode generateReportAndParseJson(String outputFile) throws IOException {
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(outputFile);
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(outputFile, PipelinePhases.PIPELINE_HIERARCHY);
tracker.startClass("TestClassA");
tracker.startPhase();
- tracker.endPhase(PerformanceTracker.PHASE_BUILDER_DEFINITION_EXTRACTION);
+ tracker.endPhase(PipelinePhases.PHASE_BUILDER_DEFINITION_EXTRACTION);
tracker.startGenerator();
tracker.endGenerator("FieldSupplierGenerator");
tracker.startEnhancer();
@@ -59,7 +62,7 @@ private JsonNode generateReportAndParseJson(String outputFile) throws IOExceptio
tracker.startClass("TestClassB");
tracker.startPhase();
- tracker.endPhase(PerformanceTracker.PHASE_DTO_MAPPING);
+ tracker.endPhase(PipelinePhases.PHASE_DTO_MAPPING);
tracker.endClass(3, 1);
CapturingProcessingLogger capturing = CapturingProcessingLogger.create();
@@ -72,7 +75,8 @@ private JsonNode generateReportAndParseJson(String outputFile) throws IOExceptio
@Test
void generateReport_withNoData_logsBasicReport() {
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(null);
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(null, PipelinePhases.PIPELINE_HIERARCHY);
CapturingProcessingLogger capturing = CapturingProcessingLogger.create();
ProcessingLogger logger = capturing.logger();
@@ -85,7 +89,8 @@ void generateReport_withNoData_logsBasicReport() {
@Test
void generateReport_withClassData_logsClassCount() {
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(null);
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(null, PipelinePhases.PIPELINE_HIERARCHY);
tracker.startClass("MyClass");
tracker.endClass(4, 1);
@@ -100,7 +105,8 @@ void generateReport_withClassData_logsClassCount() {
@Test
void generateReport_withGeneratorData_logsGeneratorStats() {
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(null);
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(null, PipelinePhases.PIPELINE_HIERARCHY);
tracker.startClass("MyClass");
tracker.startGenerator();
tracker.endGenerator("MyGenerator");
@@ -116,7 +122,8 @@ void generateReport_withGeneratorData_logsGeneratorStats() {
@Test
void generateReport_withEnhancerData_logsEnhancerStats() {
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(null);
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(null, PipelinePhases.PIPELINE_HIERARCHY);
tracker.startClass("MyClass");
tracker.startEnhancer();
tracker.endEnhancer("MyEnhancer");
@@ -132,11 +139,12 @@ void generateReport_withEnhancerData_logsEnhancerStats() {
@Test
void generateReport_withPhaseData_logsPhaseBreakdown() {
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(null);
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(null, PipelinePhases.PIPELINE_HIERARCHY);
tracker.startPhase();
- tracker.endPhase(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION);
+ tracker.endPhase(PipelinePhases.PHASE_CONFIGURATION_RESOLUTION);
tracker.startPhase();
- tracker.endPhase(PerformanceTracker.PHASE_CODE_GENERATION);
+ tracker.endPhase(PipelinePhases.PHASE_CODE_GENERATION);
CapturingProcessingLogger capturing = CapturingProcessingLogger.create();
ProcessingLogger logger = capturing.logger();
@@ -145,17 +153,18 @@ void generateReport_withPhaseData_logsPhaseBreakdown() {
assertTrue(capturing.messages().stream().anyMatch(m -> m.contains("Phase breakdown")));
assertTrue(
capturing.messages().stream()
- .anyMatch(m -> m.contains(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION)));
+ .anyMatch(m -> m.contains(PipelinePhases.PHASE_CONFIGURATION_RESOLUTION)));
assertTrue(
capturing.messages().stream()
- .anyMatch(m -> m.contains(PerformanceTracker.PHASE_CODE_GENERATION)));
+ .anyMatch(m -> m.contains(PipelinePhases.PHASE_CODE_GENERATION)));
}
@Test
void generateReport_withElementCollectionPhase_logsPhaseInReport() {
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(null);
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(null, PipelinePhases.PIPELINE_HIERARCHY);
tracker.startPhase();
- tracker.endPhase(PerformanceTracker.PHASE_ELEMENT_COLLECTION);
+ tracker.endPhase(PipelinePhases.PHASE_ELEMENT_COLLECTION);
CapturingProcessingLogger capturing = CapturingProcessingLogger.create();
ProcessingLogger logger = capturing.logger();
@@ -163,16 +172,17 @@ void generateReport_withElementCollectionPhase_logsPhaseInReport() {
assertTrue(
capturing.messages().stream()
- .anyMatch(m -> m.contains(PerformanceTracker.PHASE_ELEMENT_COLLECTION)),
- "Text report should contain " + PerformanceTracker.PHASE_ELEMENT_COLLECTION);
+ .anyMatch(m -> m.contains(PipelinePhases.PHASE_ELEMENT_COLLECTION)),
+ "Text report should contain " + PipelinePhases.PHASE_ELEMENT_COLLECTION);
}
@Test
void jsonReport_phaseBreakdown_containsElementCollectionPhase() throws IOException {
Path jsonFile = tempDir.resolve("report-element-collection.json");
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString());
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(jsonFile.toString(), PipelinePhases.PIPELINE_HIERARCHY);
tracker.startPhase();
- tracker.endPhase(PerformanceTracker.PHASE_ELEMENT_COLLECTION);
+ tracker.endPhase(PipelinePhases.PHASE_ELEMENT_COLLECTION);
CapturingProcessingLogger capturing = CapturingProcessingLogger.create();
ProcessingLogger logger = capturing.logger();
@@ -181,9 +191,9 @@ void jsonReport_phaseBreakdown_containsElementCollectionPhase() throws IOExcepti
JsonNode root = new ObjectMapper().readTree(Files.readString(jsonFile));
JsonNode phases = root.get("phaseBreakdown");
assertTrue(
- phases.has(PerformanceTracker.PHASE_ELEMENT_COLLECTION),
- "JSON phaseBreakdown should contain " + PerformanceTracker.PHASE_ELEMENT_COLLECTION);
- JsonNode elementCollection = phases.get(PerformanceTracker.PHASE_ELEMENT_COLLECTION);
+ phases.has(PipelinePhases.PHASE_ELEMENT_COLLECTION),
+ "JSON phaseBreakdown should contain " + PipelinePhases.PHASE_ELEMENT_COLLECTION);
+ JsonNode elementCollection = phases.get(PipelinePhases.PHASE_ELEMENT_COLLECTION);
assertTrue(elementCollection.has("elapsedNanos"));
assertTrue(elementCollection.has("elapsedSeconds"));
assertTrue(elementCollection.has("percentage"));
@@ -191,7 +201,8 @@ void jsonReport_phaseBreakdown_containsElementCollectionPhase() throws IOExcepti
@Test
void generateReport_withNullOutputFile_doesNotWriteFile() {
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(null);
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(null, PipelinePhases.PIPELINE_HIERARCHY);
tracker.startClass("MyClass");
tracker.endClass(1, 0);
@@ -273,12 +284,12 @@ void jsonReport_phaseBreakdown_containsPhaseHierarchy() throws IOException {
JsonNode root = generateReportAndParseJson(jsonFile.toString());
JsonNode phases = root.get("phaseBreakdown");
- assertTrue(phases.has(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION));
- assertTrue(phases.has(PerformanceTracker.PHASE_BUILDER_DEFINITION_EXTRACTION));
- assertTrue(phases.has(PerformanceTracker.PHASE_DTO_MAPPING));
- assertTrue(phases.has(PerformanceTracker.PHASE_CODE_GENERATION));
+ 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_CODE_GENERATION));
- JsonNode codeGen = phases.get(PerformanceTracker.PHASE_CODE_GENERATION);
+ JsonNode codeGen = phases.get(PipelinePhases.PHASE_CODE_GENERATION);
assertTrue(codeGen.has("elapsedNanos"));
assertTrue(codeGen.has("elapsedSeconds"));
assertTrue(codeGen.has("percentage"));
@@ -289,17 +300,18 @@ void jsonReport_phaseBreakdown_codeGenerationHasChildren() throws IOException {
Path jsonFile = tempDir.resolve("report.json");
JsonNode root = generateReportAndParseJson(jsonFile.toString());
- JsonNode codeGen = root.get("phaseBreakdown").get(PerformanceTracker.PHASE_CODE_GENERATION);
+ JsonNode codeGen = root.get("phaseBreakdown").get(PipelinePhases.PHASE_CODE_GENERATION);
assertTrue(codeGen.has("children"));
JsonNode children = codeGen.get("children");
- assertTrue(children.has(PerformanceTracker.PHASE_SOURCE_CONSTRUCTION));
- assertTrue(children.has(PerformanceTracker.PHASE_FILE_WRITING));
+ assertTrue(children.has(GenerationPhases.PHASE_SOURCE_CONSTRUCTION));
+ assertTrue(children.has(GenerationPhases.PHASE_FILE_WRITING));
}
@Test
void jsonReport_withNoData_hasEmptyArrays() throws IOException {
Path jsonFile = tempDir.resolve("report-empty.json");
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString());
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(jsonFile.toString(), PipelinePhases.PIPELINE_HIERARCHY);
CapturingProcessingLogger capturing = CapturingProcessingLogger.create();
ProcessingLogger logger = capturing.logger();
@@ -326,8 +338,9 @@ void jsonReport_createsParentDirectoriesIfMissing() throws IOException {
@Test
void endMethodsWithoutStart_doesNothing() {
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(null);
- tracker.endPhase(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION);
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(null, PipelinePhases.PIPELINE_HIERARCHY);
+ tracker.endPhase(PipelinePhases.PHASE_CONFIGURATION_RESOLUTION);
tracker.endGenerator("NonexistentGenerator");
tracker.endEnhancer("NonexistentEnhancer");
tracker.endClass(5, 2);
@@ -346,7 +359,8 @@ void endMethodsWithoutStart_doesNothing() {
@Test
void multipleGenerators_accumulateTimeAndCalls() throws IOException {
Path jsonFile = tempDir.resolve("report-multi-gen.json");
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString());
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(jsonFile.toString(), PipelinePhases.PIPELINE_HIERARCHY);
tracker.startClass("MyClass");
tracker.startGenerator();
tracker.endGenerator("GenA");
@@ -371,7 +385,8 @@ void multipleGenerators_accumulateTimeAndCalls() throws IOException {
@Test
void jsonReport_escapesSpecialCharactersInNames() throws IOException {
Path jsonFile = tempDir.resolve("report-escaping.json");
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString());
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(jsonFile.toString(), PipelinePhases.PIPELINE_HIERARCHY);
tracker.startClass("MyClass");
tracker.startGenerator();
tracker.endGenerator("Gen\"\\\n\t\r\b\fA\u0001");
@@ -392,26 +407,27 @@ void jsonReport_escapesSpecialCharactersInNames() throws IOException {
@Test
void multiplePhases_accumulateTime() throws IOException {
Path jsonFile = tempDir.resolve("report-multi-phase.json");
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString());
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(jsonFile.toString(), PipelinePhases.PIPELINE_HIERARCHY);
tracker.startPhase();
- tracker.endPhase(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION);
+ tracker.endPhase(PipelinePhases.PHASE_CONFIGURATION_RESOLUTION);
tracker.startPhase();
- tracker.endPhase(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION);
+ tracker.endPhase(PipelinePhases.PHASE_CONFIGURATION_RESOLUTION);
CapturingProcessingLogger capturing = CapturingProcessingLogger.create();
ProcessingLogger logger = capturing.logger();
tracker.generateReport(logger);
JsonNode root = new ObjectMapper().readTree(Files.readString(jsonFile));
- JsonNode phase =
- root.get("phaseBreakdown").get(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION);
+ JsonNode phase = root.get("phaseBreakdown").get(PipelinePhases.PHASE_CONFIGURATION_RESOLUTION);
assertTrue(phase.get("elapsedNanos").asLong() > 0);
}
@Test
void generateReport_logsWarningOnInvalidPath() {
ActivePerformanceTracker tracker =
- new ActivePerformanceTracker("/nonexistent\0invalid/path.json");
+ new ActivePerformanceTracker(
+ "/nonexistent\0invalid/path.json", PipelinePhases.PIPELINE_HIERARCHY);
tracker.startClass("MyClass");
tracker.endClass(1, 0);
@@ -446,7 +462,8 @@ void noOpPerformanceTracker_allMethodsAreNoOps() {
@Test
void jsonReport_generatorAndEnhancerStats_sortedByElapsedDesc() throws IOException {
Path jsonFile = tempDir.resolve("report-sorted.json");
- ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString());
+ ActivePerformanceTracker tracker =
+ new ActivePerformanceTracker(jsonFile.toString(), PipelinePhases.PIPELINE_HIERARCHY);
tracker.startClass("MyClass");
tracker.startGenerator();
tracker.endGenerator("SlowGen");
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 3ee63ad0..c524a352 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
@@ -76,21 +76,20 @@ private CapturingProcessingLogger(List
- *
+ *
+ *
+ *
+ *
+ *
+ * @param reader the compiler arguments reader to read option values through
+ * @param logger the logger for debug output
+ * @return a BuilderConfiguration with values read from compiler arguments
+ */
+ public static BuilderConfiguration readCompilerConfiguration(
+ CompilerArgumentsReader reader, ProcessingLogger logger) {
+ BuilderConfiguration.Builder builder = BuilderConfiguration.builder();
+ Stream.of(CompilerArgumentsEnum.values())
+ .filter(option -> !option.isAnnotationOnly())
+ .forEach(option -> option.apply(builder, reader.readValue(option), logger));
+ return builder.build();
+ }
+
/**
* Resolves the complete builder configuration for an element by chaining all configuration
* sources in priority order.
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 3a8ce4a1..4c75d9a7 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
@@ -46,6 +46,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.javahelpers.simple.builders.core.annotations.IgnoreInBuilder;
+import org.javahelpers.simple.builders.core.annotations.SimpleBuilderConstructor;
import org.javahelpers.simple.builders.core.enums.AccessModifier;
import org.javahelpers.simple.builders.core.util.TrackedValue;
import org.javahelpers.simple.builders.processor.analysis.FieldAnnotationExtractor;
@@ -489,7 +490,8 @@ private static List
- *
- *
- *
- *
- *
- * @return a BuilderConfiguration with values read from compiler arguments
- */
- public BuilderConfiguration readBuilderConfiguration(ProcessingLogger logger) {
- BuilderConfiguration.Builder builder = BuilderConfiguration.builder();
- Stream.of(CompilerArgumentsEnum.values())
- .filter(option -> !option.isAnnotationOnly())
- .forEach(option -> option.apply(builder, readValue(option), logger));
- return builder.build();
- }
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerOption.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerOption.java
new file mode 100644
index 00000000..dbc54d53
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerOption.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.simple.builders.processor.processing;
+
+/**
+ * A compiler option that can be read from the annotation processing environment.
+ *
+ *