From 3c750be9def7ecdb8ac70d02a8a5a8f0b87d317d Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 12 Oct 2026 00:34:46 +0000
Subject: [PATCH 1/6] processor: decouple codegen layer from builder internals
(phase 0)
---
.../builders/processor/BuilderProcessor.java | 84 +++++--
.../analysis/FieldAnnotationExtractor.java | 13 +-
.../processor/analysis/JavaLangAnalyser.java | 27 ++-
.../processor/analysis/JavaLangMapper.java | 117 +++++-----
.../analysis/TypeMappingContext.java | 144 ++++++++++++
.../classgen/ClassCodeGenerator.java | 48 ++++
.../classgen/CodeGenerationException.java | 54 +++++
.../classgen/GenerationEnvironment.java | 97 +++++++++
.../classgen/GenerationIssueReporter.java | 42 ++++
.../processor/classgen/GenerationPhases.java | 52 +++++
.../processor/classgen/SourceFormatter.java | 45 ++++
.../roaster/RoasterCodeGenerator.java | 205 ++++++++----------
.../roaster/RoasterSourceFormatter.java | 19 +-
.../classgen/roaster/package-info.java | 4 +-
.../BuilderConfigurationReader.java | 52 +++++
.../processing/BuilderDefinitionCreator.java | 7 +-
.../processing/CompilerArgumentsEnum.java | 2 +-
.../processing/CompilerArgumentsReader.java | 72 +-----
.../processor/processing/CompilerOption.java | 49 +++++
.../processor/processing/PipelinePhases.java | 84 +++++++
.../processing/ProcessingContext.java | 65 ++++--
.../processing/SourceFormatterResolver.java | 72 ++++++
.../logging/ActivePerformanceTracker.java | 59 ++---
.../logging/PerformanceTracker.java | 25 ---
.../processing/logging/PhaseHierarchy.java | 51 +++++
.../processing/logging/ProcessingLogger.java | 56 +++--
.../processor/BuilderScopeResolverTest.java | 2 +-
.../CompilerArgumentsReaderTest.java | 29 ++-
.../RoasterCodeGeneratorResilienceTest.java | 44 +++-
.../roaster/RoasterSourceFormatterTest.java | 18 +-
.../logging/ActivePerformanceTrackerTest.java | 101 +++++----
.../testing/CapturingProcessingLogger.java | 9 +-
32 files changed, 1285 insertions(+), 463 deletions(-)
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/CodeGenerationException.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationIssueReporter.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/SourceFormatter.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerOption.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/processing/PipelinePhases.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java
create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PhaseHierarchy.java
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 extractDeprecatedAnnotation(
- Element element, ProcessingContext context) {
+ Element element, TypeMappingContext context) {
if (element == null) {
return Optional.empty();
}
@@ -120,7 +119,7 @@ public static Optional extractDeprecatedAnnotation(
* @return list of annotations to be copied to the builder field
*/
public static List extractAnnotations(
- VariableElement param, ProcessingContext context) {
+ VariableElement param, TypeMappingContext context) {
List annotations = new ArrayList<>();
List extends AnnotationMirror> annotationMirrors = param.getAnnotationMirrors();
@@ -140,7 +139,7 @@ public static List extractAnnotations(
* @return list of annotations to be copied to the builder field
*/
public static List extractAnnotations(
- javax.lang.model.type.TypeMirror typeMirror, ProcessingContext context) {
+ javax.lang.model.type.TypeMirror typeMirror, TypeMappingContext context) {
List annotations = new ArrayList<>();
List extends AnnotationMirror> annotationMirrors = typeMirror.getAnnotationMirrors();
@@ -160,7 +159,7 @@ public static List extractAnnotations(
* @return Optional containing the extracted annotation, or empty if it should be skipped
*/
private static Optional extractAnnotation(
- AnnotationMirror mirror, ProcessingContext context) {
+ AnnotationMirror mirror, TypeMappingContext context) {
// Get the annotation type element
Element annotationElement = mirror.getAnnotationType().asElement();
if (!(annotationElement instanceof TypeElement annotationType)) {
@@ -188,7 +187,7 @@ private static Optional extractAnnotation(
* @return Optional containing the extracted annotation
*/
private static Optional extractAnnotationWithoutFiltering(
- AnnotationMirror mirror, ProcessingContext context) {
+ AnnotationMirror mirror, TypeMappingContext context) {
Element annotationElement = mirror.getAnnotationType().asElement();
if (!(annotationElement instanceof TypeElement annotationType)) {
return Optional.empty();
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 95a584b7..15e812cf 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
@@ -49,7 +49,6 @@
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
-import org.javahelpers.simple.builders.core.annotations.SimpleBuilderConstructor;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.ProcessingContext;
@@ -204,9 +203,9 @@ public static boolean isSetterForField(ExecutableElement mth) {
* @param context processing context
* @return {@code true}, if the element has an accessible empty constructor
*/
- public static boolean hasEmptyConstructor(TypeElement typeElement, ProcessingContext context) {
+ public static boolean hasEmptyConstructor(TypeElement typeElement, TypeMappingContext context) {
return ElementFilter.constructorsIn(context.getAllMembers(typeElement)).stream()
- .filter(context::isMemberAccessibleFromBuilderPackage)
+ .filter(context::isAccessibleFromGeneratedPackage)
.anyMatch(JavaLangAnalyser::hasNoParameters);
}
@@ -389,7 +388,7 @@ private static List findMethods(
return List.of();
}
return ElementFilter.methodsIn(context.getAllMembers(type)).stream()
- .filter(context::isMemberAccessibleFromBuilderPackage)
+ .filter(context::isAccessibleFromGeneratedPackage)
.filter(method -> returnTypeMatcher.test(method.getReturnType()))
.filter(method -> hasParameters(method, expectedParameterTypes))
.toList();
@@ -510,7 +509,7 @@ public static List findFieldFunctions(
return ElementFilter.methodsIn(context.getAllMembers(type)).stream()
.filter(JavaLangAnalyser::isNotStatic)
.filter(method -> isNoMethodOfObjectClass(method, context))
- .filter(context::isMemberAccessibleFromBuilderPackage)
+ .filter(context::isAccessibleFromGeneratedPackage)
.filter(method -> method.getParameters().size() == 1)
.toList();
}
@@ -548,7 +547,7 @@ public static boolean hasConstructorAccepting(
return false;
}
return ElementFilter.constructorsIn(context.getAllMembers(builderType)).stream()
- .filter(context::isMemberAccessibleFromBuilderPackage)
+ .filter(context::isAccessibleFromGeneratedPackage)
.anyMatch(constructor -> hasParameters(constructor, List.of(expectedType)));
}
@@ -847,24 +846,28 @@ public static Optional findSetterForField(
/**
* Determines which constructor to use for builder initialization. Prioritizes constructors
- * annotated with {@link SimpleBuilderConstructor}. If none is annotated, selects the constructor
+ * annotated with the given constructor annotation. If none is annotated, selects the constructor
* with the highest number of parameters. Returns empty if no constructor has parameters (i.e.,
* only default constructor or none found).
*
* @param annotatedType the type element to search for constructors
+ * @param constructorAnnotation the marker annotation selecting a constructor explicitly; may be
+ * {@code null} to skip the annotation check
* @param context the processing context providing access to elements and types utilities
* @return Optional containing the selected constructor, or empty if none suitable
*/
public static Optional findConstructorForBuilder(
- TypeElement annotatedType, ProcessingContext context) {
+ TypeElement annotatedType,
+ Class extends Annotation> constructorAnnotation,
+ ProcessingContext context) {
List ctors =
ElementFilter.constructorsIn(context.getAllMembers(annotatedType)).stream()
- .filter(context::isMemberAccessibleFromBuilderPackage)
+ .filter(context::isAccessibleFromGeneratedPackage)
.toList();
- // First, check if any constructor is annotated with @SimpleBuilderConstructor
+ // First, check if any constructor is annotated with the constructor annotation
for (ExecutableElement ctor : ctors) {
- if (ctor.getAnnotation(SimpleBuilderConstructor.class) != null) {
+ if (constructorAnnotation != null && ctor.getAnnotation(constructorAnnotation) != null) {
return Optional.of(ctor);
}
}
@@ -894,6 +897,6 @@ public static Optional findConstructorForBuilder(
public static boolean hasAccessibleConstructor(
TypeElement typeElement, ProcessingContext context) {
return ElementFilter.constructorsIn(context.getAllMembers(typeElement)).stream()
- .anyMatch(context::isMemberAccessibleFromBuilderPackage);
+ .anyMatch(context::isAccessibleFromGeneratedPackage);
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
index cd15fa12..bbbe38c8 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java
@@ -51,7 +51,6 @@
import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive;
import org.javahelpers.simple.builders.processor.model.type.TypeNameSet;
import org.javahelpers.simple.builders.processor.model.type.TypeNameVariable;
-import org.javahelpers.simple.builders.processor.processing.ProcessingContext;
/** Helper functions to create simple builder types from java.lang types. */
public final class JavaLangMapper {
@@ -74,11 +73,11 @@ public static TypeName map2TypeName(Class> clazz) {
* Maps the declared type parameters of the given type element into GenericParameterDto list.
*
* @param type the type element whose type parameters to map
- * @param context the processing context
+ * @param context the type mapping context
* @return list of GenericParameterDto representing the type parameters
*/
public static List map2GenericParameterDtos(
- TypeElement type, ProcessingContext context) {
+ TypeElement type, TypeMappingContext context) {
return type.getTypeParameters().stream()
.map(tp -> map2GenericParameterDto(tp, context))
.toList();
@@ -88,11 +87,11 @@ public static List map2GenericParameterDtos(
* Maps a single {@code TypeParameterElement} to {@code GenericParameterDto}.
*
* @param tp the type parameter element to map
- * @param context the processing context
+ * @param context the type mapping context
* @return a GenericParameterDto representing the type parameter
*/
public static GenericParameterDto map2GenericParameterDto(
- TypeParameterElement tp, ProcessingContext context) {
+ TypeParameterElement tp, TypeMappingContext context) {
GenericParameterDto g = new GenericParameterDto();
g.setName(tp.getSimpleName().toString());
tp.getBounds().stream()
@@ -107,11 +106,11 @@ public static GenericParameterDto map2GenericParameterDto(
* org.javahelpers.simple.builders.processor.dtos.MethodParameterDto}.
*
* @param param Parameter variable to be mapped
- * @param context processing context
+ * @param context the type mapping context
* @return MethodParameterDto holding the information of the param.
*/
public static MethodParameterDto map2MethodParameter(
- VariableElement param, ProcessingContext context) {
+ VariableElement param, TypeMappingContext context) {
MethodParameterDto result = new MethodParameterDto();
result.setParameterName(param.getSimpleName().toString());
TypeMirror typeMirror = param.asType();
@@ -134,10 +133,10 @@ public static MethodParameterDto map2MethodParameter(
* without resolving builder or constructor information.
*
* @param typeElement the TypeElement to map
- * @param context the processing context
+ * @param context the type mapping context
* @return TypeName of the type element, or null if the type element is null
*/
- public static TypeName mapToTypeName(TypeElement typeElement, ProcessingContext context) {
+ public static TypeName mapToTypeName(TypeElement typeElement, TypeMappingContext context) {
if (typeElement == null) {
return null;
}
@@ -151,10 +150,10 @@ public static TypeName mapToTypeName(TypeElement typeElement, ProcessingContext
* Maps a {@code TypeElement} to a simple-builder {@code TypeName}.
*
* @param typeElement the TypeElement to map
- * @param context the processing context
+ * @param context the type mapping context
* @return TypeName holding the information of the type element, or null if mapping fails
*/
- public static TypeName map2TypeName(TypeElement typeElement, ProcessingContext context) {
+ public static TypeName map2TypeName(TypeElement typeElement, TypeMappingContext context) {
if (typeElement == null) {
return null;
}
@@ -175,10 +174,10 @@ public static TypeName map2TypeName(TypeElement typeElement, ProcessingContext c
*
* @param typeName the TypeName to enhance with builder/constructor info
* @param typeElement the TypeElement representing the type
- * @param context the processing context
+ * @param context the type mapping context
*/
private static void setBuilderAndConstructorInfo(
- TypeName typeName, TypeElement typeElement, ProcessingContext context) {
+ TypeName typeName, TypeElement typeElement, TypeMappingContext context) {
setBuilderTypeIfScopeMatches(typeName, typeElement, context);
setEmptyConstructorInfoIfAvailable(typeName, typeElement, context);
setElementBuilderTypeIfScopeMatches(typeName, context);
@@ -189,14 +188,11 @@ private static void setBuilderAndConstructorInfo(
*
* @param typeName the TypeName to enhance
* @param typeElement the type element to check
- * @param context the processing context
+ * @param context the type mapping context
*/
private static void setBuilderTypeIfScopeMatches(
- TypeName typeName, TypeElement typeElement, ProcessingContext context) {
- context
- .getBuilderScopeResolver()
- .resolveUsableBuilderType(typeElement)
- .ifPresent(typeName::setResolvedBuilder);
+ TypeName typeName, TypeElement typeElement, TypeMappingContext context) {
+ context.resolveGeneratedType(typeElement).ifPresent(typeName::setResolvedBuilder);
}
/**
@@ -204,10 +200,10 @@ private static void setBuilderTypeIfScopeMatches(
*
* @param typeName the TypeName to enhance
* @param typeElement the type element to check
- * @param context the processing context
+ * @param context the type mapping context
*/
private static void setEmptyConstructorInfoIfAvailable(
- TypeName typeName, TypeElement typeElement, ProcessingContext context) {
+ TypeName typeName, TypeElement typeElement, TypeMappingContext context) {
if (isConcreteClass(typeElement)
&& !TypeNameAnalyser.isJavaClass(typeName)
&& JavaLangAnalyser.hasEmptyConstructor(typeElement, context)) {
@@ -220,10 +216,10 @@ private static void setEmptyConstructorInfoIfAvailable(
* configured builder scope.
*
* @param typeName the TypeName to enhance
- * @param context the processing context
+ * @param context the type mapping context
*/
private static void setElementBuilderTypeIfScopeMatches(
- TypeName typeName, ProcessingContext context) {
+ TypeName typeName, TypeMappingContext context) {
// Only process generic types
if (!(typeName instanceof TypeNameGeneric genericType)) {
return;
@@ -243,10 +239,9 @@ private static void setElementBuilderTypeIfScopeMatches(
return;
}
- // Resolve usable element builder type through the scope resolver
+ // Resolve usable element builder type through the generated type resolver
context
- .getBuilderScopeResolver()
- .resolveUsableBuilderType(elementTypeElement)
+ .resolveGeneratedType(elementTypeElement)
.ifPresent(genericType::setElementResolvedBuilder);
}
@@ -254,37 +249,25 @@ private static void setElementBuilderTypeIfScopeMatches(
* Gets the TypeElement for a given TypeName if it exists.
*
* @param typeName the type name to resolve
- * @param context the processing context
+ * @param context the type mapping context
* @return the TypeElement, or null if not found or not a TypeElement
*/
private static TypeElement retrieveTypeElementIfExists(
- TypeName typeName, ProcessingContext context) {
+ TypeName typeName, TypeMappingContext context) {
Element element = context.getTypeElement(typeName.getFullQualifiedName());
return element instanceof TypeElement typeElement ? typeElement : null;
}
- /**
- * Creates a TypeName for the builder of a given TypeElement using the configured builder suffix.
- *
- * @param typeElement the type element to create builder name for
- * @param context the processing context
- * @return the TypeName for the builder
- */
- public static TypeName createBuilderTypeName(TypeElement typeElement, ProcessingContext context) {
- return createBuilderTypeName(
- typeElement, context, context.getConfiguration().getBuilderSuffix());
- }
-
/**
* Creates a TypeName for the builder of a given TypeElement using an explicit suffix.
*
* @param typeElement the type element to create builder name for
- * @param context the processing context
+ * @param context the type mapping context
* @param suffix the builder class name suffix to append
* @return the TypeName for the builder
*/
public static TypeName createBuilderTypeName(
- TypeElement typeElement, ProcessingContext context, String suffix) {
+ TypeElement typeElement, TypeMappingContext context, String suffix) {
String builderClassName = typeElement.getSimpleName().toString() + suffix;
String packageName = context.getPackageName(typeElement);
return new TypeName(packageName, builderClassName);
@@ -295,10 +278,10 @@ public static TypeName createBuilderTypeName(
*
* @param typeName the TypeName to enhance with builder/constructor info
* @param param the VariableElement representing the parameter
- * @param context the processing context
+ * @param context the type mapping context
*/
private static void setBuilderAndConstructorInfo(
- TypeName typeName, VariableElement param, ProcessingContext context) {
+ TypeName typeName, VariableElement param, TypeMappingContext context) {
Element element = context.asElement(param.asType());
if (element instanceof TypeElement typeElement) {
setBuilderAndConstructorInfo(typeName, typeElement, context);
@@ -307,10 +290,10 @@ private static void setBuilderAndConstructorInfo(
/**
* Maps a list of {@code TypeMirror} to a list of simple-builder {@code TypeName}s using {@link
- * #extractType(TypeMirror, ProcessingContext)}.
+ * #extractType(TypeMirror, TypeMappingContext)}.
*/
private static List extractTypeForList(
- List typeMirrors, ProcessingContext context) {
+ List typeMirrors, TypeMappingContext context) {
List result = new ArrayList<>(typeMirrors.size());
for (TypeMirror tm : typeMirrors) {
result.add(extractType(tm, context));
@@ -329,12 +312,15 @@ private static List extractTypeForList(
* @param rawType the base TypeName (preserves concrete class information)
* @param argTypes the generic type arguments from the class declaration (may not match interface)
* @param typeMirror the TypeMirror to check for interface implementation
- * @param context the processing context
+ * @param context the type mapping context
* @return a specialized TypeName (TypeNameList, TypeNameSet, TypeNameMap) if applicable, or a
* TypeNameGeneric/rawType otherwise
*/
private static TypeName wrapInCollectionTypeIfApplicable(
- TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) {
+ TypeName rawType,
+ List argTypes,
+ TypeMirror typeMirror,
+ TypeMappingContext context) {
TypeName listWrapper = tryWrapAsList(rawType, argTypes, typeMirror, context);
if (listWrapper != null) {
return listWrapper;
@@ -361,7 +347,10 @@ private static TypeName wrapInCollectionTypeIfApplicable(
* @return TypeNameList if applicable, null otherwise
*/
private static TypeName tryWrapAsList(
- TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) {
+ TypeName rawType,
+ List argTypes,
+ TypeMirror typeMirror,
+ TypeMappingContext context) {
TypeElement listElement = context.getTypeElement("java.util.List");
if (listElement == null) {
return null;
@@ -390,7 +379,10 @@ private static TypeName tryWrapAsList(
* @return TypeNameSet if applicable, null otherwise
*/
private static TypeName tryWrapAsSet(
- TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) {
+ TypeName rawType,
+ List argTypes,
+ TypeMirror typeMirror,
+ TypeMappingContext context) {
TypeElement setElement = context.getTypeElement("java.util.Set");
if (setElement == null) {
return null;
@@ -419,7 +411,10 @@ private static TypeName tryWrapAsSet(
* @return TypeNameMap if applicable, null otherwise
*/
private static TypeName tryWrapAsMap(
- TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) {
+ TypeName rawType,
+ List argTypes,
+ TypeMirror typeMirror,
+ TypeMappingContext context) {
TypeElement mapElement = context.getTypeElement("java.util.Map");
if (mapElement == null) {
return null;
@@ -451,11 +446,11 @@ private static TypeName tryWrapAsMap(
*
* @param typeElement the type to check
* @param interfaceElement the collection interface (List or Set)
- * @param context the processing context
+ * @param context the type mapping context
* @return true if the type is the interface itself or has a Collection constructor
*/
private static boolean shouldWrapAsCollectionType(
- TypeElement typeElement, TypeElement interfaceElement, ProcessingContext context) {
+ TypeElement typeElement, TypeElement interfaceElement, TypeMappingContext context) {
boolean isInterface = typeElement.equals(interfaceElement);
boolean hasConstructor =
!isInterface
@@ -484,11 +479,11 @@ private static TypeName createFallbackTypeName(TypeName rawType, List
* @param typeElement the type to check
* @param parameterTypeName the fully qualified name of the parameter type (e.g.,
* "java.util.Collection")
- * @param context the processing context
+ * @param context the type mapping context
* @return true if the type has a constructor accepting the specified parameter type
*/
private static boolean hasConstructorWithParameterOfType(
- TypeElement typeElement, String parameterTypeName, ProcessingContext context) {
+ TypeElement typeElement, String parameterTypeName, TypeMappingContext context) {
TypeElement parameterElement = context.getTypeElement(parameterTypeName);
if (parameterElement == null) {
return false;
@@ -520,11 +515,11 @@ private static boolean hasConstructorWithParameterOfType(
*
* @param typeMirror the type to examine
* @param targetInterface the interface element (e.g., List, Set, Map)
- * @param context the processing context
+ * @param context the type mapping context
* @return list of type arguments used by the interface, or empty list if raw type
*/
public static List extractInterfaceTypeArguments(
- TypeMirror typeMirror, TypeElement targetInterface, ProcessingContext context) {
+ TypeMirror typeMirror, TypeElement targetInterface, TypeMappingContext context) {
// Walk the supertype hierarchy to find the specific instantiation of the target interface
TypeMirror found = findSupertype(typeMirror, targetInterface, context);
@@ -543,11 +538,11 @@ public static List extractInterfaceTypeArguments(
*
* @param typeMirror the type to search from
* @param targetInterface the interface to find
- * @param context the processing context
+ * @param context the type mapping context
* @return the matching supertype, or null if not found
*/
private static TypeMirror findSupertype(
- TypeMirror typeMirror, TypeElement targetInterface, ProcessingContext context) {
+ TypeMirror typeMirror, TypeElement targetInterface, TypeMappingContext context) {
if (!(typeMirror instanceof DeclaredType)) {
return null;
}
@@ -568,7 +563,7 @@ private static TypeMirror findSupertype(
return null;
}
- private static TypeName extractType(TypeMirror typeOfParameter, ProcessingContext context) {
+ private static TypeName extractType(TypeMirror typeOfParameter, TypeMappingContext context) {
TypeName typeName =
typeOfParameter.accept(
new SimpleTypeVisitor14() {
@@ -638,7 +633,7 @@ protected TypeName defaultAction(TypeMirror e, Void p) {
},
null);
- if (typeName != null && context.getConfiguration().shouldCopyTypeAnnotations()) {
+ if (typeName != null && context.shouldCopyTypeAnnotations()) {
List annotations =
FieldAnnotationExtractor.extractAnnotations(typeOfParameter, context);
annotations.forEach(typeName::addAnnotation);
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java
new file mode 100644
index 00000000..8d783ad4
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java
@@ -0,0 +1,144 @@
+/*
+ * 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.analysis;
+
+import java.util.List;
+import java.util.Optional;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeMirror;
+import org.javahelpers.simple.builders.processor.model.type.ResolvedBuilder;
+
+/**
+ * Everything {@link JavaLangMapper} needs for mapping elements and type mirrors to {@code TypeName}
+ * instances: javax.lang.model lookups plus the two mapping decisions (annotation copying and
+ * generated-type resolution). Implemented by the processing context; the mapper stays independent
+ * of builder internals.
+ */
+public interface TypeMappingContext {
+
+ /**
+ * Resolves a type element by its fully qualified name.
+ *
+ * @param qualifiedName fully qualified type name
+ * @return the type element, or {@code null} if it cannot be resolved
+ */
+ TypeElement getTypeElement(String qualifiedName);
+
+ /**
+ * Returns the package name of the given element.
+ *
+ * @param element the element
+ * @return the qualified package name
+ */
+ String getPackageName(Element element);
+
+ /**
+ * Returns the element corresponding to a type mirror.
+ *
+ * @param typeMirror the type mirror
+ * @return the corresponding element
+ */
+ Element asElement(TypeMirror typeMirror);
+
+ /**
+ * Returns the erasure of a type mirror: the raw type with generic information stripped (JLS
+ * §4.6). A parameterized type like {@code List} erases to the raw type {@code List}, and
+ * a type variable erases to its leftmost bound.
+ *
+ * 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
+ */
+ 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
+ */
+ 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 resolveGeneratedType(TypeElement typeElement);
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java
new file mode 100644
index 00000000..e1357c07
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java
@@ -0,0 +1,48 @@
+/*
+ * 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 org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
+
+/**
+ * SPI for class code generators: turns a {@link GenerationTargetClassDto} into source code and
+ * writes it through the processing environment's filer.
+ *
+ * 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..b37c8a71 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,84 @@
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 formatterCache =
- new EnumMap<>(FormattingMode.class);
+ /** Logger for debug output during code generation. */
+ private final ProcessingLogger logger;
- /**
- * Creates a code generator using the given processing context and environment.
- *
- * @param context processing context providing logger, performance tracking, type lookup and
- * formatter profile
- * @param processingEnv processing environment providing the filer for generated source files
- */
- public RoasterCodeGenerator(ProcessingContext context, ProcessingEnvironment processingEnv) {
- this.context = Objects.requireNonNull(context, "context must not be null");
- this.processingEnv = Objects.requireNonNull(processingEnv, "processingEnv must not be null");
- }
+ /** Performance tracker for sub-phase timing (Source Construction, File Writing). */
+ private final PerformanceTracker performanceTracker;
+
+ /** Reporter for problems found during generation; the reporter decides the severity. */
+ private final GenerationIssueReporter issueReporter;
+
+ /** Factory for creating a source formatter per formatting mode. */
/**
- * Returns a cached or newly created {@link RoasterSourceFormatter} for the given mode.
+ * Creates a code generator using the given generation environment.
*
- * At most 3 formatter instances exist (one per {@link FormattingMode} enum value), created
- * lazily on first use.
- *
- * @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 +162,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 +202,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 +218,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 +244,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 +268,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 resolvedMethods = resolveMethodConflicts(classDef.getMethods());
- context.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size());
+ logger.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size());
for (MethodDto methodDto : resolvedMethods) {
appendMethod(source, methodDto, false, false);
}
- context.debugEndOperation("Methods added: %d", resolvedMethods.size());
- context.endPerformancePhase(PHASE_METHODS);
+ logger.debugEndOperation("Methods added: %d", resolvedMethods.size());
+ performanceTracker.endPhase(PHASE_METHODS);
}
/**
@@ -301,7 +303,7 @@ private List resolveMethodConflicts(List methods) {
if (seenSignatures.add(method.getSignatureKey())) {
deduplicated.add(method);
} else {
- context.reportBasedOnStrictMode(
+ issueReporter.report(
"Unexpected duplicate method signature: '%s' — keeping first occurrence (safety net)",
method.getSignatureKey());
}
@@ -320,16 +322,16 @@ private void appendMethod(
}
private void appendNestedTypes(JavaClassSource source, GenerationTargetClassDto classDef) {
- context.startPerformancePhase();
+ performanceTracker.startPhase();
if (CollectionUtils.isNotEmpty(classDef.getNestedTypes())) {
- context.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size());
+ logger.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size());
for (NestedTypeDto nestedType : classDef.getNestedTypes()) {
appendNestedType(source, nestedType);
- context.debug("Generated nested type: %s", nestedType.getTypeName());
+ logger.debug("Generated nested type: %s", nestedType.getTypeName());
}
- context.debugEndOperation("Nested types added");
+ logger.debugEndOperation("Nested types added");
}
- context.endPerformancePhase(PHASE_NESTED_TYPES);
+ performanceTracker.endPhase(PHASE_NESTED_TYPES);
}
private void appendNestedType(JavaClassSource source, NestedTypeDto nestedType) {
@@ -556,30 +558,17 @@ private void addParameter(
applyAnnotations(parameter, paramDto.getAnnotations());
}
- private String formatSource(String rawSource, FormattingMode mode) {
- return getFormatter(mode).format(rawSource);
+ private String formatSource(String rawSource, SourceFormatter formatter) {
+ return formatter.format(rawSource);
}
private void writeClassToFile(String sourceCode, GenerationTargetClassDto classDef)
- throws BuilderException {
- context.debug(
+ throws CodeGenerationException {
+ logger.debug(
"Writing class to file: %s.%s",
classDef.getTypeName().getPackageName(), classDef.getTypeName().getClassName());
String qualifiedName = classDef.getTypeName().getFullQualifiedName();
- if (builderClassAlreadyExists(qualifiedName)) {
- throw new BuilderException(
- null,
- """
- 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));
- }
-
try {
JavaFileObject file = processingEnv.getFiler().createSourceFile(qualifiedName);
try (Writer writer = file.openWriter()) {
@@ -594,25 +583,7 @@ private void writeClassToFile(String sourceCode, GenerationTargetClassDto classD
"""
.formatted(
qualifiedName, StringUtils.isNotBlank(message) ? message : "Unknown error");
- throw new BuilderException(null, errorMessage);
- }
- }
-
- /**
- * Checks if a builder class already exists by attempting to find the type element.
- *
- * @param qualifiedName the fully qualified name of the class to check
- * @return true if the class already exists, false otherwise
- */
- private boolean builderClassAlreadyExists(String qualifiedName) {
- try {
- TypeElement existingType = context.getTypeElement(qualifiedName);
- return existingType != null;
- } catch (Exception e) {
- context.debug(
- "Error checking if builder class '%s' already exists: %s",
- qualifiedName, StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "No message");
- return false;
+ throw new CodeGenerationException(errorMessage);
}
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatter.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatter.java
index dfde5b76..f0720e82 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatter.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatter.java
@@ -33,6 +33,7 @@
import java.util.Properties;
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.processing.logging.ProcessingLogger;
import org.jboss.forge.roaster.Roaster;
import org.jboss.forge.roaster.model.util.FormatterProfileReader;
@@ -50,15 +51,15 @@
*
* 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:
*
*
- * - {@link org.javahelpers.simple.builders.processor.exceptions.BuilderException} - Exception
- * for code generation errors
+ *
- {@link org.javahelpers.simple.builders.processor.classgen.CodeGenerationException} -
+ * Exception for code generation errors
*
- {@link
* org.javahelpers.simple.builders.processor.classgen.roaster.exceptions.RoasterMapperException}
* - Exception for mapping errors
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 01bcd15c..e2cfedb4 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
@@ -30,6 +30,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.stream.Stream;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
@@ -101,6 +102,57 @@ public BuilderConfiguration getGlobalConfiguration() {
return globalConfiguration;
}
+ /**
+ * Reads a complete BuilderConfiguration from compiler arguments.
+ *
+ *
This method reads all configuration options from compiler arguments like:
+ *
+ *
+ * - {@code -Dsimplebuilder.generateFieldSupplier=true} (JVM system property, highest
+ * precedence)
+ *
- {@code -Asimplebuilder.generateFieldSupplier=true} (compiler argument)
+ *
- {@code -AgenerateFieldSupplier=true} (bare option name, backward compatibility)
+ *
- {@code -Asimplebuilder.builderAccess=public}
+ *
- etc.
+ *
+ *
+ * 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:
+ *
+ *
+ * - {@code CompilerArgumentsEnum} — add the enum constant with its applier (or pass {@code
+ * annotationOnly = true} if the option must not be set globally).
+ *
- {@code BuilderConfiguration} — add the field, builder method, merge logic, and a typed
+ * accessor (e.g. {@code formattingModeEnum}) if enum conversion is needed. Set the default
+ * in {@code BuilderConfiguration.DEFAULT}.
+ *
- {@code ProcessingContext} — should NOT need a dedicated field or getter. The resolved
+ * per-target config ({@code context.getConfiguration()}) and global config ({@code
+ * context.getConfigurationReader().getGlobalConfiguration()}) carry all option values.
+ * Special-casing outside {@code BuilderConfiguration} breaks the merge chain and bypasses
+ * annotation overrides.
+ *
+ *
+ * @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 extractConstructorFields(
ProcessingContext context,
Map fieldNameRegistry) {
List constructorFields = new LinkedList<>();
- Optional constructorOpt = findConstructorForBuilder(annotatedType, context);
+ Optional constructorOpt =
+ findConstructorForBuilder(annotatedType, SimpleBuilderConstructor.class, context);
if (constructorOpt.isPresent()) {
ExecutableElement ctor = constructorOpt.get();
context.debugStartOperation(
@@ -1181,7 +1183,8 @@ private static void applyDeprecationSuppressions(
*/
private static boolean isConstructorDeprecated(
TypeElement annotatedType, ProcessingContext context) {
- return JavaLangAnalyser.findConstructorForBuilder(annotatedType, context)
+ return JavaLangAnalyser.findConstructorForBuilder(
+ annotatedType, SimpleBuilderConstructor.class, context)
.filter(ctor -> ctor.getAnnotation(Deprecated.class) != null)
.isPresent();
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java
index 0374aa4f..66fef459 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java
@@ -44,7 +44,7 @@
*
* 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 true}.
*
- * @param argument the compiler argument enum to read
+ * @param argument the compiler option to read
* @return the value of the compiler argument, or null if not set
*/
- public String readValue(CompilerArgumentsEnum argument) {
+ public String readValue(CompilerOption argument) {
return readValue(argument, processingEnv.getOptions());
}
@@ -70,11 +66,11 @@ public String readValue(CompilerArgumentsEnum argument) {
* Reads the value of a compiler argument from an explicit options map — the same lookup SPI
* adapters can use on the options map their host framework hands them.
*
- * @param argument the compiler argument enum to read
+ * @param argument the compiler option to read
* @param options the compiler options map to search
* @return the value of the compiler argument, or null if not set
*/
- public static String readValue(CompilerArgumentsEnum argument, Map options) {
+ public static String readValue(CompilerOption argument, Map options) {
// Try the -D JVM system property first (e.g., -Dsimplebuilder.verbose)
String value = System.getProperty(argument.getCompilerArgument());
@@ -96,10 +92,10 @@ public static String readValue(CompilerArgumentsEnum argument, MapReturns true if the value equals "true" (case-insensitive), false otherwise.
*
- * @param argument the compiler argument enum to read
+ * @param argument the compiler option to read
* @return true if the value is "true" (case-insensitive), false otherwise
*/
- public boolean readBooleanValue(CompilerArgumentsEnum argument) {
+ public boolean readBooleanValue(CompilerOption argument) {
return readBooleanValue(argument, processingEnv.getOptions(), false);
}
@@ -107,14 +103,14 @@ public boolean readBooleanValue(CompilerArgumentsEnum argument) {
* 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.
*
- * @param argument the compiler argument enum to read
+ * @param argument the compiler option to read
* @param options the compiler options map to search
* @param defaultValue the result for an unset or unrecognizable value
* @return "true"/"enabled" resolves to true, "false"/"disabled" to false, anything else to {@code
* defaultValue}
*/
public static boolean readBooleanValue(
- CompilerArgumentsEnum argument, Map options, boolean defaultValue) {
+ CompilerOption argument, Map options, boolean defaultValue) {
String value = readValue(argument, options);
if (value == null || value.isEmpty()) {
return defaultValue;
@@ -127,52 +123,4 @@ public static boolean readBooleanValue(
}
return defaultValue;
}
-
- /**
- * Reads a complete BuilderConfiguration from compiler arguments.
- *
- * This method reads all configuration options from compiler arguments like:
- *
- *
- * - {@code -Dsimplebuilder.generateFieldSupplier=true} (JVM system property, highest
- * precedence)
- *
- {@code -Asimplebuilder.generateFieldSupplier=true} (compiler argument)
- *
- {@code -AgenerateFieldSupplier=true} (bare option name, backward compatibility)
- *
- {@code -Asimplebuilder.builderAccess=public}
- *
- etc.
- *
- *
- * 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:
- *
- *
- * - {@code CompilerArgumentsEnum} — add the enum constant with its applier (or pass {@code
- * annotationOnly = true} if the option must not be set globally).
- *
- {@code BuilderConfiguration} — add the field, builder method, merge logic, and a typed
- * accessor (e.g. {@code formattingModeEnum}) if enum conversion is needed. Set the default
- * in {@code BuilderConfiguration.DEFAULT}.
- *
- {@code ProcessingContext} — should NOT need a dedicated field or getter. The resolved
- * per-target config ({@code context.getConfiguration()}) and global config ({@code
- * context.getConfigurationReader().getGlobalConfiguration()}) carry all option values.
- * Special-casing outside {@code BuilderConfiguration} breaks the merge chain and bypasses
- * annotation overrides.
- *
- *
- * @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.
+ *
+ * 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 TOP_LEVEL_PHASES =
+ List.of(
+ PHASE_ELEMENT_COLLECTION,
+ PHASE_CONFIGURATION_RESOLUTION,
+ PHASE_BUILDER_DEFINITION_EXTRACTION,
+ PHASE_DTO_MAPPING,
+ PHASE_CODE_GENERATION);
+
+ /** Phase hierarchy for report display: parent phase to ordered child phases. */
+ private static final Map> PHASE_CHILDREN = phaseChildren();
+
+ /** The complete phase hierarchy of this pipeline for the performance report. */
+ public static final PhaseHierarchy PIPELINE_HIERARCHY =
+ new PhaseHierarchy(TOP_LEVEL_PHASES, PHASE_CHILDREN);
+
+ private static Map> phaseChildren() {
+ Map> children = new LinkedHashMap<>();
+ children.put(PHASE_CODE_GENERATION, List.of(PHASE_SOURCE_CONSTRUCTION, PHASE_FILE_WRITING));
+ children.put(
+ PHASE_SOURCE_CONSTRUCTION,
+ List.of(PHASE_ELEMENT_BUILDING, PHASE_STRING_GENERATION, PHASE_FORMATTING));
+ children.put(
+ PHASE_ELEMENT_BUILDING,
+ List.of(
+ PHASE_CLASS_CREATION,
+ PHASE_CLASS_METADATA,
+ PHASE_FIELDS,
+ PHASE_CONSTRUCTORS,
+ PHASE_METHODS,
+ PHASE_NESTED_TYPES,
+ PHASE_CLASS_ANNOTATIONS));
+ return children;
+ }
+
+ private PipelinePhases() {}
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java
index 1243f36e..5f87e2e0 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java
@@ -27,6 +27,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
@@ -38,12 +39,11 @@
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
-import org.apache.commons.lang3.StringUtils;
-import org.javahelpers.simple.builders.core.enums.FormattingMode;
import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver;
-import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterSourceFormatter;
+import org.javahelpers.simple.builders.processor.analysis.TypeMappingContext;
import org.javahelpers.simple.builders.processor.generators.registry.GeneratorRegistry;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
+import org.javahelpers.simple.builders.processor.model.type.ResolvedBuilder;
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;
@@ -56,15 +56,15 @@
*
* 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 resolveGeneratedType(TypeElement typeElement) {
+ return builderScopeResolver.resolveUsableBuilderType(typeElement);
+ }
+
/**
* Get the TypeElement for a given qualified class name.
*
* @param qualifiedName the canonical class name (e.g., "java.lang.String")
* @return the type element, or null if not found
*/
+ @Override
public TypeElement getTypeElement(String qualifiedName) {
return elementUtils.getTypeElement(qualifiedName);
}
@@ -289,6 +308,7 @@ public PackageElement getPackageElement(String name) {
* @param element the element
* @return the qualified package name
*/
+ @Override
public String getPackageName(Element element) {
return elementUtils.getPackageOf(element).getQualifiedName().toString();
}
@@ -300,6 +320,7 @@ public String getPackageName(Element element) {
* @return list of all members
*/
@SuppressWarnings("java:S1452")
+ @Override
public List extends Element> getAllMembers(TypeElement typeElement) {
return elementUtils.getAllMembers(typeElement);
}
@@ -320,6 +341,7 @@ public String getDocComment(Element element) {
* @param typeMirror the type mirror
* @return the element, or null if not representable as an element
*/
+ @Override
public Element asElement(TypeMirror typeMirror) {
return typeUtils.asElement(typeMirror);
}
@@ -331,6 +353,7 @@ public Element asElement(TypeMirror typeMirror) {
* @param type2 second type
* @return true if the types are the same
*/
+ @Override
public boolean isSameType(TypeMirror type1, TypeMirror type2) {
return typeUtils.isSameType(type1, type2);
}
@@ -341,6 +364,7 @@ public boolean isSameType(TypeMirror type1, TypeMirror type2) {
* @param typeMirror the type to erase
* @return the erasure of the type
*/
+ @Override
public TypeMirror erasure(TypeMirror typeMirror) {
return typeUtils.erasure(typeMirror);
}
@@ -352,6 +376,7 @@ public TypeMirror erasure(TypeMirror typeMirror) {
* @param type2 the target type
* @return true if type1 is assignable to type2
*/
+ @Override
public boolean isAssignable(TypeMirror type1, TypeMirror type2) {
return typeUtils.isAssignable(type1, type2);
}
@@ -362,6 +387,7 @@ public boolean isAssignable(TypeMirror type1, TypeMirror type2) {
* @param typeMirror the type
* @return list of direct supertypes
*/
+ @Override
public java.util.List extends TypeMirror> directSupertypes(TypeMirror typeMirror) {
return typeUtils.directSupertypes(typeMirror);
}
@@ -401,6 +427,7 @@ public void debug(String message) {
* @param format the format string
* @param args arguments referenced by the format specifiers in the format string
*/
+ @Override
public void debug(String format, Object... args) {
logger.debug(format, args);
}
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
new file mode 100644
index 00000000..92bb00ad
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.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.simple.builders.processor.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.
+ *
+ * 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 formatterCache =
+ new EnumMap<>(FormattingMode.class);
+
+ /**
+ * Creates a resolver reading the formatter profile from compiler arguments.
+ *
+ * @param processingEnv the processing environment (for compiler argument access)
+ * @param logger logger for formatter warnings
+ */
+ public SourceFormatterResolver(ProcessingEnvironment processingEnv, ProcessingLogger logger) {
+ this.logger = logger;
+ this.formatterProfile =
+ StringUtils.trimToNull(
+ new CompilerArgumentsReader(processingEnv)
+ .readValue(CompilerArgumentsEnum.FORMATTER_PROFILE));
+ }
+
+ /**
+ * Returns the formatter for the given mode, creating it on first resolve.
+ *
+ * @param mode the formatting mode
+ * @return a cached or new formatter instance
+ */
+ public SourceFormatter resolve(FormattingMode mode) {
+ return formatterCache.computeIfAbsent(
+ mode, m -> new RoasterSourceFormatter(logger, m, formatterProfile));
+ }
+}
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 be243479..bc0759d8 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
@@ -48,9 +48,9 @@
* start} pushes a timestamp onto the stack, and the matching {@code end} pops it and accumulates
* the elapsed time. This allows nested calls without passing identifiers.
*
- * 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 TOP_LEVEL_PHASES =
- List.of(
- PHASE_ELEMENT_COLLECTION,
- PHASE_CONFIGURATION_RESOLUTION,
- PHASE_BUILDER_DEFINITION_EXTRACTION,
- PHASE_DTO_MAPPING,
- PHASE_CODE_GENERATION);
-
private static final String JSON_KEY_ELAPSED_NANOS = "elapsedNanos";
- private static final Map> PHASE_CHILDREN = new LinkedHashMap<>();
-
- static {
- PHASE_CHILDREN.put(
- PHASE_CODE_GENERATION, List.of(PHASE_SOURCE_CONSTRUCTION, PHASE_FILE_WRITING));
- PHASE_CHILDREN.put(
- PHASE_SOURCE_CONSTRUCTION,
- List.of(PHASE_ELEMENT_BUILDING, PHASE_STRING_GENERATION, PHASE_FORMATTING));
- PHASE_CHILDREN.put(
- PHASE_ELEMENT_BUILDING,
- List.of(
- PHASE_CLASS_CREATION,
- PHASE_CLASS_METADATA,
- PHASE_FIELDS,
- PHASE_CONSTRUCTORS,
- PHASE_METHODS,
- PHASE_NESTED_TYPES,
- PHASE_CLASS_ANNOTATIONS));
- }
+ /** Top-level phases in report display order. */
+ private final PhaseHierarchy phaseHierarchy;
+ /** Phase hierarchy for report display: parent phase to ordered child phases. */
private final Map phaseTimes = new ConcurrentHashMap<>();
+
private final Map generatorTimes = new ConcurrentHashMap<>();
private final Map generatorCalls = new ConcurrentHashMap<>();
private final Map enhancerTimes = new ConcurrentHashMap<>();
@@ -116,13 +92,16 @@ public final class ActivePerformanceTracker implements PerformanceTracker {
private final ThreadLocal classStartTime = new ThreadLocal<>();
/**
- * Creates a new ActivePerformanceTracker and records the overall start time.
+ * Creates a new ActivePerformanceTracker with a phase hierarchy for report display and records
+ * the overall start time.
*
* @param outputFilePath optional path for JSON report output; null or empty disables file output
+ * @param phaseHierarchy the phase vocabulary the report is rendered with
*/
- public ActivePerformanceTracker(String outputFilePath) {
+ public ActivePerformanceTracker(String outputFilePath, PhaseHierarchy phaseHierarchy) {
this.totalStartTime = System.nanoTime();
this.outputFilePath = outputFilePath;
+ this.phaseHierarchy = phaseHierarchy;
}
@Override
@@ -209,11 +188,11 @@ public void generateReport(ProcessingLogger logger) {
logger.info(String.format(Locale.US, "Total processing time: %.1fs", totalSeconds));
logger.info("");
- // Phase breakdown (hierarchical, using hardcoded hierarchy)
+ // Phase breakdown (hierarchical, using configured hierarchy)
logger.info("Phase breakdown:");
- for (int i = 0; i < TOP_LEVEL_PHASES.size(); i++) {
- reportPhase(
- logger, TOP_LEVEL_PHASES.get(i), totalSeconds, "", i == TOP_LEVEL_PHASES.size() - 1);
+ List topLevelPhases = phaseHierarchy.topLevelPhases();
+ for (int i = 0; i < topLevelPhases.size(); i++) {
+ reportPhase(logger, topLevelPhases.get(i), totalSeconds, "", i == topLevelPhases.size() - 1);
}
logger.info("");
@@ -356,7 +335,7 @@ private void reportPhase(
logger.info(
String.format(
Locale.US, "%s%s%s: %.1fs (%.1f%%)", prefix, connector, phase, seconds, percentage));
- List children = PHASE_CHILDREN.get(phase);
+ List children = phaseHierarchy.subPhases().get(phase);
if (children != null) {
String childPrefix = prefix + (isLast ? " " : "│ ");
for (int i = 0; i < children.size(); i++) {
@@ -388,7 +367,7 @@ private void writeJsonReport(long totalNanos) throws IOException {
// Phase breakdown (hierarchical)
Map phaseBreakdown = new LinkedHashMap<>();
- for (String phase : TOP_LEVEL_PHASES) {
+ for (String phase : phaseHierarchy.topLevelPhases()) {
phaseBreakdown.put(phase, buildPhaseJson(phase, totalNanos));
}
root.put("phaseBreakdown", phaseBreakdown);
@@ -435,7 +414,7 @@ private Map buildPhaseJson(String phase, long parentNanos) {
phaseMap.put(JSON_KEY_ELAPSED_NANOS, nanos);
phaseMap.put("elapsedSeconds", seconds);
phaseMap.put("percentage", percentage);
- List children = PHASE_CHILDREN.get(phase);
+ List children = phaseHierarchy.subPhases().get(phase);
if (children != null && !children.isEmpty()) {
Map childrenMap = new LinkedHashMap<>();
for (String child : children) {
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PerformanceTracker.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PerformanceTracker.java
index bb787dab..dc6c1e00 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PerformanceTracker.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PerformanceTracker.java
@@ -40,31 +40,6 @@
*/
public interface PerformanceTracker {
- // Top-level phases
- String PHASE_ELEMENT_COLLECTION = "Element Collection";
- String PHASE_CONFIGURATION_RESOLUTION = "Configuration Resolution";
- String PHASE_BUILDER_DEFINITION_EXTRACTION = "Builder Definition Extraction";
- String PHASE_DTO_MAPPING = "DTO Mapping";
- String PHASE_CODE_GENERATION = "Code Generation";
-
- // Code Generation children
- String PHASE_SOURCE_CONSTRUCTION = "Source Construction";
- String PHASE_FILE_WRITING = "File Writing";
-
- // Source Construction children
- String PHASE_ELEMENT_BUILDING = "Element Building";
- String PHASE_STRING_GENERATION = "String Generation";
- String PHASE_FORMATTING = "Formatting";
-
- // Element Building children
- String PHASE_CLASS_CREATION = "Class Creation";
- String PHASE_CLASS_METADATA = "Class Metadata";
- String PHASE_FIELDS = "Fields";
- String PHASE_CONSTRUCTORS = "Constructors";
- String PHASE_METHODS = "Methods";
- String PHASE_NESTED_TYPES = "Nested Types";
- String PHASE_CLASS_ANNOTATIONS = "Class Annotations";
-
/**
* Starts tracking a processing phase.
*
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PhaseHierarchy.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PhaseHierarchy.java
new file mode 100644
index 00000000..c0db5228
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PhaseHierarchy.java
@@ -0,0 +1,51 @@
+/*
+ * 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.logging;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The phase vocabulary a {@link PerformanceTracker} report is rendered as a tree: the top-level
+ * phases in display order, plus for each parent phase the ordered sub-phases nested underneath.
+ * Phases absent from {@code subPhases} are leaves.
+ *
+ * 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 topLevelPhases, Map> subPhases) {
+
+ /** Defensive copies: hierarchy structure must not change after tracker creation. */
+ public PhaseHierarchy {
+ topLevelPhases = List.copyOf(topLevelPhases);
+ Map> copied = new LinkedHashMap<>();
+ subPhases.forEach((parent, children) -> copied.put(parent, List.copyOf(children)));
+ subPhases = Map.copyOf(copied);
+ }
+}
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 c67fc9cb..171eb9bb 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
@@ -28,8 +28,6 @@
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.tools.Diagnostic;
-import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsEnum;
-import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader;
/**
* Logger for all messages during annotation processing. Providing util-functions for posting
@@ -40,26 +38,48 @@ public class ProcessingLogger {
/** Util class for exposing messages of type {@code javax.annotation.processing.Messager}. */
private final Messager messager;
- /** Flag indicating if debug logging is enabled. */
- private final boolean debugEnabled;
+ /** Verbosity level of this logger. */
+ private final Verbosity verbosity;
/** Thread-local indentation level for hierarchical logging. */
private static final ThreadLocal indentationLevel = ThreadLocal.withInitial(() -> 0);
+ /** Verbosity level of a {@link ProcessingLogger}. */
+ private enum Verbosity {
+ /** Errors, warnings and info messages only. */
+ NORMAL,
+ /** Additionally reports debug messages. */
+ VERBOSE
+ }
+
/**
- * Constructs a new ProcessingLogger with the specified ProcessingEnvironment. The Messager is
- * used to report errors, warnings, and other notices during annotation processing. Debug logging
- * is enabled by setting the compiler argument: -Averbose=true or -Asimplebuilder.verbose=true
+ * Creates a new ProcessingLogger with the specified ProcessingEnvironment. The Messager is used
+ * to report errors, warnings, and other notices during annotation processing. To enable debug
+ * messages use {@link #createVerbose(ProcessingEnvironment)} instead.
*
- * @param processingEnv the processing environment providing messager and options
+ * @param processingEnv the processing environment providing the messager
+ * @return a processing logger reporting errors, warnings and info messages
*/
- public ProcessingLogger(ProcessingEnvironment processingEnv) {
+ public static ProcessingLogger create(ProcessingEnvironment processingEnv) {
+ return new ProcessingLogger(processingEnv, Verbosity.NORMAL);
+ }
+
+ /**
+ * Creates a new ProcessingLogger that additionally reports debug messages.
+ *
+ * @param processingEnv the processing environment providing the messager
+ * @return a debug-enabled processing logger
+ */
+ public static ProcessingLogger createVerbose(ProcessingEnvironment processingEnv) {
+ return new ProcessingLogger(processingEnv, Verbosity.VERBOSE);
+ }
+
+ private ProcessingLogger(ProcessingEnvironment processingEnv, Verbosity verbosity) {
// Reset ThreadLocal state to ensure clean state between test runs
resetThreadLocalState();
this.messager = processingEnv.getMessager();
- CompilerArgumentsReader reader = new CompilerArgumentsReader(processingEnv);
- this.debugEnabled = reader.readBooleanValue(CompilerArgumentsEnum.VERBOSE);
+ this.verbosity = verbosity;
}
/** Resets ThreadLocal state to ensure clean state between test runs. */
@@ -120,7 +140,7 @@ private String formatHierarchicalMessage(String message, int spaces) {
*/
public void info(String format, Object... args) {
String message = String.format(format, args);
- if (debugEnabled) {
+ if (verbosity == Verbosity.VERBOSE) {
// When debug is enabled, add spaces to align with [DEBUG] prefix (which is 6 characters
// longer)
String indentedMessage = formatHierarchicalMessage(message, 8); // Add 8 spaces for alignment
@@ -139,7 +159,7 @@ public void info(String format, Object... args) {
* @param message the debug message to be posted
*/
public void debug(String message) {
- if (debugEnabled) {
+ if (verbosity == Verbosity.VERBOSE) {
String indentedMessage = formatWithIndentation(message);
messager.printMessage(Diagnostic.Kind.OTHER, indentedMessage);
}
@@ -153,7 +173,7 @@ public void debug(String message) {
* @param args arguments referenced by the format specifiers in the format string
*/
public void debug(String format, Object... args) {
- if (debugEnabled) {
+ if (verbosity == Verbosity.VERBOSE) {
String message = String.format(format, args);
String indentedMessage = formatWithIndentation(message);
messager.printMessage(Diagnostic.Kind.OTHER, indentedMessage);
@@ -169,7 +189,7 @@ public void debug(String format, Object... args) {
*/
public void warning(String format, Object... args) {
String message = String.format(format, args);
- if (debugEnabled) {
+ if (verbosity == Verbosity.VERBOSE) {
// When debug is enabled, add spaces to align with [DEBUG] prefix (which is 5 characters
// longer)
String indentedMessage = formatHierarchicalMessage(message, 5); // Add 5 spaces for alignment
@@ -189,7 +209,7 @@ public void warning(String format, Object... args) {
*/
public void warning(Element e, String format, Object... args) {
String message = String.format(format, args);
- if (debugEnabled) {
+ if (verbosity == Verbosity.VERBOSE) {
// When debug is enabled, add spaces to align with [DEBUG] prefix (which is 5 characters
// longer)
String indentedMessage = formatHierarchicalMessage(message, 5); // Add 5 spaces for alignment
@@ -269,7 +289,7 @@ public void debugStartOperation(String formatClosingMessage, Object... args) {
int currentLevel = indentationLevel.get();
// Log the operation start message with proper prefix handling
- if (debugEnabled) {
+ if (verbosity == Verbosity.VERBOSE) {
String operationMessage = formatWithIndentation(String.format(formatClosingMessage, args));
messager.printMessage(Diagnostic.Kind.NOTE, operationMessage);
}
@@ -302,7 +322,7 @@ public void debugEndOperation(String formatClosingMessage, Object... args) {
int currentLevel = indentationLevel.get();
// Log the closing message with └─ to indicate it's the last operation at this level
- if (debugEnabled) {
+ if (verbosity == Verbosity.VERBOSE) {
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/BuilderScopeResolverTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderScopeResolverTest.java
index bf6851b4..32678366 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
@@ -1316,7 +1316,7 @@ public boolean process(Set extends TypeElement> annotations, RoundEnvironment
TypeElement helper = processingEnv.getElementUtils().getTypeElement("lib.LibHelper");
ProcessingContext context =
new ProcessingContext(
- new ProcessingLogger(processingEnv), BuilderConfiguration.DEFAULT, processingEnv);
+ ProcessingLogger.create(processingEnv), BuilderConfiguration.DEFAULT, processingEnv);
context.initProcessingTarget(new ProcessingTarget(configuration("lib", "Builder"), ""));
BuilderScopeResolver resolver = context.getBuilderScopeResolver();
// Register the type as generated, mirroring the real processor which calls
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java
index 76e1247c..5fbe9b00 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java
@@ -33,6 +33,7 @@
import org.javahelpers.simple.builders.core.enums.AccessModifier;
import org.javahelpers.simple.builders.core.enums.OptionState;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
+import org.javahelpers.simple.builders.processor.processing.BuilderConfigurationReader;
import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsEnum;
import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader;
import org.javahelpers.simple.builders.processor.testing.CapturingProcessingLogger;
@@ -250,14 +251,15 @@ void readBooleanValue_ExplicitOptionsMap_CompilerArgumentBeatsBareOption() {
true));
}
- /** Test: readBuilderConfiguration with no arguments returns all UNSET/DEFAULT values. */
+ /** Test: readCompilerConfiguration with no arguments returns all UNSET/DEFAULT values. */
@Test
- void readBuilderConfiguration_NoArguments_ReturnsDefaults() {
+ void readCompilerConfiguration_NoArguments_ReturnsDefaults() {
ProcessingEnvironment env = ProcessingEnvironmentStub.createEmpty();
CompilerArgumentsReader reader = new CompilerArgumentsReader(env);
BuilderConfiguration config =
- reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger());
+ BuilderConfigurationReader.readCompilerConfiguration(
+ reader, CapturingProcessingLogger.create().logger());
assertNotNull(config, "Configuration should not be null");
assertEquals(OptionState.UNSET, config.generateFieldSupplier());
@@ -276,9 +278,9 @@ void readBuilderConfiguration_NoArguments_ReturnsDefaults() {
"Builder usage packages should be empty when not set");
}
- /** Test: readBuilderConfiguration reads all options correctly. */
+ /** Test: readCompilerConfiguration reads all options correctly. */
@Test
- void readBuilderConfiguration_AllOptionsSet_ReadsCorrectly() {
+ void readCompilerConfiguration_AllOptionsSet_ReadsCorrectly() {
ProcessingEnvironment env =
ProcessingEnvironmentStub.builder()
.put("simplebuilder.generateFieldSupplier", "true")
@@ -298,7 +300,8 @@ void readBuilderConfiguration_AllOptionsSet_ReadsCorrectly() {
CompilerArgumentsReader reader = new CompilerArgumentsReader(env);
BuilderConfiguration config =
- reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger());
+ BuilderConfigurationReader.readCompilerConfiguration(
+ reader, CapturingProcessingLogger.create().logger());
assertEquals(OptionState.ENABLED, config.generateFieldSupplier());
assertEquals(OptionState.DISABLED, config.generateFieldConsumer());
@@ -315,9 +318,9 @@ void readBuilderConfiguration_AllOptionsSet_ReadsCorrectly() {
assertEquals(Set.of("x.y"), config.getBuilderUsagePackagesSet());
}
- /** Test: readBuilderConfiguration handles mixed valid and invalid values. */
+ /** Test: readCompilerConfiguration handles mixed valid and invalid values. */
@Test
- void readBuilderConfiguration_MixedValidInvalid_HandlesGracefully() {
+ void readCompilerConfiguration_MixedValidInvalid_HandlesGracefully() {
ProcessingEnvironment env =
ProcessingEnvironmentStub.builder()
.put("simplebuilder.generateFieldSupplier", "invalid")
@@ -327,7 +330,8 @@ void readBuilderConfiguration_MixedValidInvalid_HandlesGracefully() {
CompilerArgumentsReader reader = new CompilerArgumentsReader(env);
BuilderConfiguration config =
- reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger());
+ BuilderConfigurationReader.readCompilerConfiguration(
+ reader, CapturingProcessingLogger.create().logger());
assertEquals(
OptionState.UNSET, config.generateFieldSupplier(), "Invalid option should be UNSET");
@@ -339,9 +343,9 @@ void readBuilderConfiguration_MixedValidInvalid_HandlesGracefully() {
OptionState.ENABLED, config.generateFieldConsumer(), "Valid option should be ENABLED");
}
- /** Test: readBuilderConfiguration with empty string values. */
+ /** Test: readCompilerConfiguration with empty string values. */
@Test
- void readBuilderConfiguration_EmptyStringValues_HandlesGracefully() {
+ void readCompilerConfiguration_EmptyStringValues_HandlesGracefully() {
ProcessingEnvironment env =
ProcessingEnvironmentStub.builder()
.put("simplebuilder.generateFieldSupplier", "")
@@ -352,7 +356,8 @@ void readBuilderConfiguration_EmptyStringValues_HandlesGracefully() {
CompilerArgumentsReader reader = new CompilerArgumentsReader(env);
BuilderConfiguration config =
- reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger());
+ BuilderConfigurationReader.readCompilerConfiguration(
+ reader, CapturingProcessingLogger.create().logger());
assertEquals(OptionState.UNSET, config.generateFieldSupplier(), "Empty should be UNSET");
assertEquals(AccessModifier.DEFAULT, config.getBuilderAccess(), "Empty should be DEFAULT");
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 4c221819..5f2bd82d 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
@@ -30,9 +30,10 @@
import javax.annotation.processing.ProcessingEnvironment;
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.roaster.RoasterCodeGenerator;
import org.javahelpers.simple.builders.processor.classgen.roaster.exceptions.RoasterMapperException;
-import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto;
import org.javahelpers.simple.builders.processor.model.method.ConstructorDto;
@@ -42,6 +43,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.ProcessingContext;
+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;
@@ -51,9 +53,10 @@
* Regression tests for {@link RoasterCodeGenerator} error-handling robustness.
*
* 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 +68,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 +83,20 @@ 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));
+ CodeGenerationException thrown =
+ assertThrows(
+ CodeGenerationException.class,
+ () ->
+ generator.generateClass(
+ classDef,
+ new SourceFormatterResolver(env, logger).resolve(FormattingMode.NONE)));
assertInstanceOf(RoasterMapperException.class, thrown.getCause());
}
@@ -109,10 +120,21 @@ 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));
// 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,
+ new SourceFormatterResolver(env, capturing.logger()).resolve(FormattingMode.NONE)));
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..799fde13 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
@@ -560,14 +560,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("class A {}");
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("class A {}");
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 +609,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("class A {}");
capturing.assertMessage(
"OTHER: [DEBUG] Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'.");
}
@@ -636,7 +639,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("class A {}");
capturing.assertMessage(
"OTHER: [DEBUG] Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'.");
}
@@ -645,7 +648,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("class A {}");
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 messages, ProcessingLogger logger
public static CapturingProcessingLogger create() {
List messages = new ArrayList<>();
ProcessingLogger logger =
- new ProcessingLogger(createCapturingEnvironment(messages, Collections.emptyMap()));
+ ProcessingLogger.create(createCapturingEnvironment(messages, Collections.emptyMap()));
return new CapturingProcessingLogger(messages, logger);
}
/**
- * Creates a new capturing logger with debug logging enabled via {@code
- * simplebuilder.verbose=true}.
+ * Creates a new capturing logger with debug logging enabled.
*
* @return a new debug-enabled capturing logger instance
*/
public static CapturingProcessingLogger createDebugEnabled() {
List messages = new ArrayList<>();
ProcessingLogger logger =
- new ProcessingLogger(
- createCapturingEnvironment(messages, Map.of("simplebuilder.verbose", "true")));
+ ProcessingLogger.createVerbose(
+ createCapturingEnvironment(messages, Collections.emptyMap()));
return new CapturingProcessingLogger(messages, logger);
}
From 2d5eb61bde34715b5157ef0b612e3d39ec2b587e Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Thu, 22 Oct 2026 20:48:11 +0000
Subject: [PATCH 2/6] test: extract formatter creation before assertThrows in
resilience test
Keep the assertThrows lambda wrapping only generateClass, so the expected
exception is clearly attributed to the generation call.
---
.../RoasterCodeGeneratorResilienceTest.java | 18 +++++++-----------
1 file changed, 7 insertions(+), 11 deletions(-)
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 5f2bd82d..fb0940e0 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
@@ -32,6 +32,7 @@
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.exceptions.RoasterMapperException;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
@@ -90,13 +91,11 @@ void shouldWrapRenderingRuntimeExceptionInCodeGenerationException() {
new GenerationEnvironment(
env, logger, context.getPerformanceTracker(), context::reportBasedOnStrictMode));
+ SourceFormatter formatter =
+ new SourceFormatterResolver(env, logger).resolve(FormattingMode.NONE);
CodeGenerationException thrown =
assertThrows(
- CodeGenerationException.class,
- () ->
- generator.generateClass(
- classDef,
- new SourceFormatterResolver(env, logger).resolve(FormattingMode.NONE)));
+ CodeGenerationException.class, () -> generator.generateClass(classDef, formatter));
assertInstanceOf(RoasterMapperException.class, thrown.getCause());
}
@@ -128,13 +127,10 @@ void shouldKeepFirstMethodAndWarnOnDuplicateSignature() {
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,
- new SourceFormatterResolver(env, capturing.logger()).resolve(FormattingMode.NONE)));
+ assertThrows(NullPointerException.class, () -> generator.generateClass(classDef, formatter));
capturing.assertMessage(
"WARNING: Unexpected duplicate method signature: 'name(java.lang.String)'"
+ " — keeping first occurrence (safety net)");
From 21bf052365fb74579645078464ef8b47aac22f8a Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Thu, 22 Oct 2026 20:51:55 +0000
Subject: [PATCH 3/6] test: hoist 'class A {}' format trigger into
TESTDATA_CODE constant
The five constructor tests only need a compilable input to trigger the lazy
profile initialization; a named constant states that intent.
---
.../roaster/RoasterSourceFormatterTest.java | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
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 799fde13..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);
}
@@ -561,7 +564,7 @@ void constructor_lightweightMode_missingProfile_noFallbackWarning() {
CapturingProcessingLogger capturing = CapturingProcessingLogger.createDebugEnabled();
new RoasterSourceFormatter(
capturing.logger(), FormattingMode.LIGHTWEIGHT, "nonexistent-profile.xml")
- .format("class A {}");
+ .format(TESTDATA_CODE);
capturing.assertMessage("OTHER: [DEBUG] Using lightweight source formatting.");
}
@@ -569,7 +572,7 @@ void constructor_lightweightMode_missingProfile_noFallbackWarning() {
void constructor_missingProfile_logsProfileNotFoundWarning() {
CapturingProcessingLogger capturing = CapturingProcessingLogger.create();
new RoasterSourceFormatter(capturing.logger(), FormattingMode.JDT, "nonexistent-profile.xml")
- .format("class A {}");
+ .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.");
}
@@ -612,7 +615,7 @@ void constructor_classpathProfile_explicitDefaultName_noWarning() {
capturing.logger(),
FormattingMode.JDT,
RoasterSourceFormatter.DEFAULT_FORMATTER_PROFILE_RESOURCE)
- .format("class A {}");
+ .format(TESTDATA_CODE);
capturing.assertMessage(
"OTHER: [DEBUG] Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'.");
}
@@ -639,7 +642,7 @@ void constructor_missingProfile_fallsBackToBundledProfile() {
@Test
void constructor_blankProfile_usesBundledDefault() {
CapturingProcessingLogger capturing = CapturingProcessingLogger.createDebugEnabled();
- new RoasterSourceFormatter(capturing.logger(), FormattingMode.JDT, " ").format("class A {}");
+ 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'.");
}
@@ -649,7 +652,7 @@ void constructor_malformedProfile_logsLoadFailureWarning() {
CapturingProcessingLogger capturing = CapturingProcessingLogger.createDebugEnabled();
new RoasterSourceFormatter(
capturing.logger(), FormattingMode.JDT, "eclipse-java-format-malformed.xml")
- .format("class A {}");
+ .format(TESTDATA_CODE);
capturing.assertMessageMatching(
w ->
w.startsWith(
From e4d1b368717528c4d039a69f22bfa909596b7ca5 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Thu, 22 Oct 2026 21:07:05 +0000
Subject: [PATCH 4/6] classgen: suppress S1452 on javax-forced wildcard return
types
Types.directSupertypes/Elements.getAllMembers return List extends X> by
API design; same suppression the codebase already uses for such signatures.
---
.../simple/builders/processor/analysis/TypeMappingContext.java | 2 ++
.../simple/builders/processor/processing/ProcessingContext.java | 1 +
2 files changed, 3 insertions(+)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java
index 8d783ad4..543d4d9d 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java
@@ -100,6 +100,7 @@ public interface TypeMappingContext {
* @param typeMirror the type mirror
* @return list of direct supertypes
*/
+ @SuppressWarnings("java:S1452")
List extends TypeMirror> directSupertypes(TypeMirror typeMirror);
/**
@@ -108,6 +109,7 @@ public interface TypeMappingContext {
* @param typeElement the type to inspect
* @return list of all member elements
*/
+ @SuppressWarnings("java:S1452")
List extends Element> getAllMembers(TypeElement typeElement);
/**
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java
index 5f87e2e0..c26253ef 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java
@@ -387,6 +387,7 @@ public boolean isAssignable(TypeMirror type1, TypeMirror type2) {
* @param typeMirror the type
* @return list of direct supertypes
*/
+ @SuppressWarnings("java:S1452")
@Override
public java.util.List extends TypeMirror> directSupertypes(TypeMirror typeMirror) {
return typeUtils.directSupertypes(typeMirror);
From b227f1fa2ba23525149a2519487001b15af1f35b Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Thu, 22 Oct 2026 21:07:05 +0000
Subject: [PATCH 5/6] classgen: drop dangling javadoc comment left by formatter
refactor
The 'Factory for creating a source formatter' block lost its field when
RoasterSourceFormatterFactory was removed.
---
.../processor/classgen/roaster/RoasterCodeGenerator.java | 2 --
1 file changed, 2 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 b37c8a71..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
@@ -90,8 +90,6 @@ public class RoasterCodeGenerator implements ClassCodeGenerator {
/** Reporter for problems found during generation; the reporter decides the severity. */
private final GenerationIssueReporter issueReporter;
- /** Factory for creating a source formatter per formatting mode. */
-
/**
* Creates a code generator using the given generation environment.
*
From d0751f971c50c2747cfde524f332581c6a772c5b Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Thu, 22 Oct 2026 21:10:36 +0000
Subject: [PATCH 6/6] docs: note why S1452 is suppressed on javax.lang.model
signatures
The wildcard return types are enforced by Types.directSupertypes and
Elements.getAllMembers, not a design choice.
---
.../builders/processor/analysis/TypeMappingContext.java | 4 ++--
.../builders/processor/processing/ProcessingContext.java | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java
index 543d4d9d..96af362b 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java
@@ -98,7 +98,7 @@ public interface TypeMappingContext {
* Returns the direct supertypes of a type mirror.
*
* @param typeMirror the type mirror
- * @return list of direct supertypes
+ * @return list of direct supertypes (wildcard enforced by the javax.lang.model API)
*/
@SuppressWarnings("java:S1452")
List extends TypeMirror> directSupertypes(TypeMirror typeMirror);
@@ -107,7 +107,7 @@ public interface TypeMappingContext {
* Returns all members of a type, including inherited members.
*
* @param typeElement the type to inspect
- * @return list of all member elements
+ * @return list of all member elements (wildcard enforced by the javax.lang.model API)
*/
@SuppressWarnings("java:S1452")
List extends Element> getAllMembers(TypeElement typeElement);
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java
index c26253ef..e0bdf6c2 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java
@@ -317,7 +317,7 @@ public String getPackageName(Element element) {
* Get all members of a type, including inherited members.
*
* @param typeElement the type to inspect
- * @return list of all members
+ * @return list of all members (wildcard enforced by the javax.lang.model API)
*/
@SuppressWarnings("java:S1452")
@Override