From 234dc53093c9811e2d2ac847ee7335cf9fa54f3b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:16:17 +0000 Subject: [PATCH 1/2] processor: decouple codegen layer from builder internals (phase 0) Prepares the code-generation layer for extraction into a separate simple-codegen library. No file moves, no behavior change; only dependency direction changes inside the processor module. - AnnotationProcessingContext: new base class holding env/Elements/ Types/logger/tracker + element helpers; ProcessingContext extends it and keeps builder-only state (config reader, scope resolver, registry) - ProcessingLogger: takes debugEnabled flag instead of reading CompilerArgumentsEnum - CompilerOption: new interface; CompilerArgumentsEnum implements it; CompilerArgumentsReader reads CompilerOption; readBuilderConfiguration moved to BuilderConfigurationReader - JavaLangAnalyser/JavaLangMapper/FieldAnnotationExtractor: depend on AnnotationProcessingContext; findConstructorForBuilder takes the marker annotation class as a parameter; new MapperOptions carries copyTypeAnnotations + generated-type resolution into the mapper - ClassCodeGenerator SPI + GenerationEnvironment introduced; RoasterCodeGenerator implements it; BuilderProcessor goes through the interface - Phase constants split: ProcessingPhases (builder pipeline) + GenerationPhases (codegen sub-phases); PerformanceTracker is a pure protocol interface; ActivePerformanceTracker takes its phase hierarchy at construction Co-Authored-By: Andreas Igel --- .../builders/processor/BuilderProcessor.java | 28 +- .../analysis/FieldAnnotationExtractor.java | 18 +- .../processor/analysis/JavaLangAnalyser.java | 28 +- .../processor/analysis/JavaLangMapper.java | 144 ++++---- .../processor/analysis/MapperOptions.java | 57 +++ .../classgen/ClassCodeGenerator.java | 47 +++ .../classgen/GenerationEnvironment.java | 83 +++++ .../processor/classgen/GenerationPhases.java | 52 +++ .../roaster/RoasterCodeGenerator.java | 138 ++++--- .../RoasterSourceFormatterFactory.java | 40 +++ .../AnnotationProcessingContext.java | 338 ++++++++++++++++++ .../BuilderConfigurationReader.java | 50 +++ .../processing/BuilderDefinitionCreator.java | 33 +- .../processing/CompilerArgumentsEnum.java | 4 +- .../processing/CompilerArgumentsReader.java | 66 +--- .../processor/processing/CompilerOption.java | 49 +++ .../processing/ProcessingContext.java | 313 ++-------------- .../processing/ProcessingPhases.java | 79 ++++ .../logging/ActivePerformanceTracker.java | 64 ++-- .../logging/PerformanceTracker.java | 25 -- .../processing/logging/ProcessingLogger.java | 11 +- .../processor/BuilderScopeResolverTest.java | 4 +- .../CompilerArgumentsReaderTest.java | 13 +- .../RoasterCodeGeneratorResilienceTest.java | 8 +- .../logging/ActivePerformanceTrackerTest.java | 129 ++++--- .../testing/CapturingProcessingLogger.java | 8 +- 26 files changed, 1204 insertions(+), 625 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/MapperOptions.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/GenerationEnvironment.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/roaster/RoasterSourceFormatterFactory.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.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/ProcessingPhases.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 e324c169..349568b6 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.ProcessingPhases.PHASE_BUILDER_DEFINITION_EXTRACTION; +import static org.javahelpers.simple.builders.processor.processing.ProcessingPhases.PHASE_CODE_GENERATION; +import static org.javahelpers.simple.builders.processor.processing.ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION; +import static org.javahelpers.simple.builders.processor.processing.ProcessingPhases.PHASE_DTO_MAPPING; +import static org.javahelpers.simple.builders.processor.processing.ProcessingPhases.PHASE_ELEMENT_COLLECTION; import com.google.auto.service.AutoService; import java.util.ArrayList; @@ -50,6 +50,8 @@ import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template; import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser; +import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator; +import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment; import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator; @@ -77,23 +79,27 @@ public class BuilderProcessor extends AbstractProcessor { private ProcessingContext context; private ProcessingLogger logger; - private RoasterCodeGenerator codeGenerator; + private ClassCodeGenerator codeGenerator; 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 = + new ProcessingLogger(processingEnv, reader.readBooleanValue(CompilerArgumentsEnum.VERBOSE)); + logger.debug("Starting BuilderProcessor..."); + BuilderConfiguration globalConfig = + BuilderConfigurationReader.readBuilderConfiguration(reader, logger); logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig); this.context = new ProcessingContext(logger, globalConfig, processingEnv); - this.codeGenerator = new RoasterCodeGenerator(context, processingEnv); + this.codeGenerator = + new RoasterCodeGenerator( + new GenerationEnvironment(processingEnv, logger, context.getPerformanceTracker()), + context::createSourceFormatter); this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig); // Initialize GeneratorRegistry once during processor initialization diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java index d4e951ae..13345ff8 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,7 @@ 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; +import org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext; /** Extractor for field annotations, converting them from Java model elements to DTOs. */ public final class FieldAnnotationExtractor { @@ -82,9 +82,9 @@ 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 - * a single annotation and works on any {@link Element} (method, field, parameter, record - * component, type), not just {@link VariableElement}s. + *

Unlike {@link #extractAnnotations(VariableElement, AnnotationProcessingContext)}, this + * method targets a single annotation and works on any {@link Element} (method, field, parameter, + * record component, type), not just {@link VariableElement}s. * * @param element the element to inspect, or {@code null} * @param context processing context @@ -92,7 +92,7 @@ private FieldAnnotationExtractor() { * element is not deprecated */ public static Optional extractDeprecatedAnnotation( - Element element, ProcessingContext context) { + Element element, AnnotationProcessingContext context) { if (element == null) { return Optional.empty(); } @@ -120,7 +120,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, AnnotationProcessingContext context) { List annotations = new ArrayList<>(); List annotationMirrors = param.getAnnotationMirrors(); @@ -140,7 +140,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, AnnotationProcessingContext context) { List annotations = new ArrayList<>(); List annotationMirrors = typeMirror.getAnnotationMirrors(); @@ -160,7 +160,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, AnnotationProcessingContext context) { // Get the annotation type element Element annotationElement = mirror.getAnnotationType().asElement(); if (!(annotationElement instanceof TypeElement annotationType)) { @@ -188,7 +188,7 @@ private static Optional extractAnnotation( * @return Optional containing the extracted annotation */ private static Optional extractAnnotationWithoutFiltering( - AnnotationMirror mirror, ProcessingContext context) { + AnnotationMirror mirror, AnnotationProcessingContext 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 9da8610c..3f828c59 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 @@ -42,8 +42,7 @@ 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.processing.ProcessingContext; +import org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext; /** Helperclass for extrating specific information from existing classes. */ public final class JavaLangAnalyser { @@ -74,7 +73,7 @@ public static boolean isNoMethodOfObjectClass(ExecutableElement mth) { * @return filtered list excluding Object class methods */ public static List findAllPossibleSettersOfClass( - TypeElement typeElement, ProcessingContext context) { + TypeElement typeElement, AnnotationProcessingContext context) { return ElementFilter.methodsIn(context.getAllMembers(typeElement)).stream() .filter(JavaLangAnalyser::isNoMethodOfObjectClass) .filter(JavaLangAnalyser::isSetterForField) @@ -187,7 +186,8 @@ public static boolean isSetterForField(ExecutableElement mth) { * @param context processing context * @return {@code true}, if the element has an empty constructor */ - public static boolean hasEmptyConstructor(TypeElement typeElement, ProcessingContext context) { + public static boolean hasEmptyConstructor( + TypeElement typeElement, AnnotationProcessingContext context) { List constructors = ElementFilter.constructorsIn(context.getAllMembers(typeElement)); return constructors.stream().anyMatch(c -> c.getParameters().isEmpty()); @@ -232,7 +232,7 @@ public static Optional findAnnotation( * @return {@code true} if the type declares or inherits a matching {@code build()} method */ public static boolean hasBuildMethodReturning( - TypeElement builderType, String expectedReturnType, ProcessingContext context) { + TypeElement builderType, String expectedReturnType, AnnotationProcessingContext context) { if (builderType == null) { return false; } @@ -255,7 +255,7 @@ public static boolean hasBuildMethodReturning( * @return {@code true} if the type declares or inherits a matching constructor */ public static boolean hasConstructorAccepting( - TypeElement builderType, String expectedType, ProcessingContext context) { + TypeElement builderType, String expectedType, AnnotationProcessingContext context) { if (builderType == null) { return false; } @@ -474,7 +474,7 @@ public static Optional findGetterForField( TypeElement dtoType, String fieldName, TypeMirror fieldTypeMirror, - ProcessingContext context) { + AnnotationProcessingContext context) { if (dtoType == null || fieldName == null || fieldTypeMirror == null) { return Optional.empty(); } @@ -541,7 +541,7 @@ public static Optional findReco * @return Optional containing the setter ExecutableElement if found */ public static Optional findSetterForField( - TypeElement dtoType, String fieldName, ProcessingContext context) { + TypeElement dtoType, String fieldName, AnnotationProcessingContext context) { if (dtoType == null || fieldName == null) { return Optional.empty(); } @@ -559,22 +559,26 @@ 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 {@code constructorAnnotation}. 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 annotation type marking a preferred constructor, 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 constructorAnnotation, + AnnotationProcessingContext context) { List ctors = ElementFilter.constructorsIn(context.getAllMembers(annotatedType)); - // 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); } } 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 ab54646a..f0028d21 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 @@ -50,7 +50,7 @@ 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; +import org.javahelpers.simple.builders.processor.processing.AnnotationProcessingContext; /** Helper functions to create simple builder types from java.lang types. */ public final class JavaLangMapper { @@ -77,9 +77,9 @@ public static TypeName map2TypeName(Class clazz) { * @return list of GenericParameterDto representing the type parameters */ public static List map2GenericParameterDtos( - TypeElement type, ProcessingContext context) { + TypeElement type, MapperOptions options, AnnotationProcessingContext context) { return type.getTypeParameters().stream() - .map(tp -> map2GenericParameterDto(tp, context)) + .map(tp -> map2GenericParameterDto(tp, options, context)) .toList(); } @@ -91,12 +91,12 @@ public static List map2GenericParameterDtos( * @return a GenericParameterDto representing the type parameter */ public static GenericParameterDto map2GenericParameterDto( - TypeParameterElement tp, ProcessingContext context) { + TypeParameterElement tp, MapperOptions options, AnnotationProcessingContext context) { GenericParameterDto g = new GenericParameterDto(); g.setName(tp.getSimpleName().toString()); tp.getBounds().stream() .filter(b -> !"java.lang.Object".equals(b.toString())) - .map(b -> extractType(b, context)) + .map(b -> extractType(b, options, context)) .forEach(g::addUpperBound); return g; } @@ -110,18 +110,18 @@ public static GenericParameterDto map2GenericParameterDto( * @return MethodParameterDto holding the information of the param. */ public static MethodParameterDto map2MethodParameter( - VariableElement param, ProcessingContext context) { + VariableElement param, MapperOptions options, AnnotationProcessingContext context) { MethodParameterDto result = new MethodParameterDto(); result.setParameterName(param.getSimpleName().toString()); TypeMirror typeMirror = param.asType(); - TypeName typeName = extractType(typeMirror, context); + TypeName typeName = extractType(typeMirror, options, context); if (typeName == null) { return null; } // Set builder and constructor information on the TypeName (only if not already set) if (!typeName.getBuilderType().isPresent()) { - setBuilderAndConstructorInfo(typeName, param, context); + setBuilderAndConstructorInfo(typeName, param, options, context); } result.setParameterTypeName(typeName); @@ -135,17 +135,18 @@ public static MethodParameterDto map2MethodParameter( * @param context the processing 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, MapperOptions options, AnnotationProcessingContext context) { if (typeElement == null) { return null; } TypeMirror typeMirror = typeElement.asType(); - TypeName typeName = extractType(typeMirror, context); + TypeName typeName = extractType(typeMirror, options, context); // Set builder and constructor information on the TypeName (only if not already set) if (typeName != null && !typeName.getBuilderType().isPresent()) { - setBuilderAndConstructorInfo(typeName, typeElement, context); + setBuilderAndConstructorInfo(typeName, typeElement, options, context); } return typeName; @@ -159,10 +160,13 @@ public static TypeName map2TypeName(TypeElement typeElement, ProcessingContext c * @param context the processing context */ private static void setBuilderAndConstructorInfo( - TypeName typeName, TypeElement typeElement, ProcessingContext context) { - setBuilderTypeIfScopeMatches(typeName, typeElement, context); + TypeName typeName, + TypeElement typeElement, + MapperOptions options, + AnnotationProcessingContext context) { + setBuilderTypeIfScopeMatches(typeName, typeElement, options, context); setEmptyConstructorInfoIfAvailable(typeName, typeElement, context); - setElementBuilderTypeIfScopeMatches(typeName, context); + setElementBuilderTypeIfScopeMatches(typeName, options, context); } /** @@ -173,11 +177,11 @@ private static void setBuilderAndConstructorInfo( * @param context the processing context */ private static void setBuilderTypeIfScopeMatches( - TypeName typeName, TypeElement typeElement, ProcessingContext context) { - context - .getBuilderScopeResolver() - .resolveUsableBuilderType(typeElement) - .ifPresent(typeName::setBuilderType); + TypeName typeName, + TypeElement typeElement, + MapperOptions options, + AnnotationProcessingContext context) { + options.resolveGeneratedType(typeElement).ifPresent(typeName::setBuilderType); } /** @@ -188,7 +192,7 @@ private static void setBuilderTypeIfScopeMatches( * @param context the processing context */ private static void setEmptyConstructorInfoIfAvailable( - TypeName typeName, TypeElement typeElement, ProcessingContext context) { + TypeName typeName, TypeElement typeElement, AnnotationProcessingContext context) { if (isConcreteClass(typeElement) && !TypeNameAnalyser.isJavaClass(typeName) && JavaLangAnalyser.hasEmptyConstructor(typeElement, context)) { @@ -204,7 +208,7 @@ private static void setEmptyConstructorInfoIfAvailable( * @param context the processing context */ private static void setElementBuilderTypeIfScopeMatches( - TypeName typeName, ProcessingContext context) { + TypeName typeName, MapperOptions options, AnnotationProcessingContext context) { // Only process generic types if (!(typeName instanceof TypeNameGeneric genericType)) { return; @@ -225,10 +229,7 @@ private static void setElementBuilderTypeIfScopeMatches( } // Resolve usable element builder type through the scope resolver - context - .getBuilderScopeResolver() - .resolveUsableBuilderType(elementTypeElement) - .ifPresent(genericType::setElementBuilderType); + options.resolveGeneratedType(elementTypeElement).ifPresent(genericType::setElementBuilderType); } /** @@ -239,23 +240,11 @@ private static void setElementBuilderTypeIfScopeMatches( * @return the TypeElement, or null if not found or not a TypeElement */ private static TypeElement retrieveTypeElementIfExists( - TypeName typeName, ProcessingContext context) { + TypeName typeName, AnnotationProcessingContext 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. * @@ -265,7 +254,7 @@ public static TypeName createBuilderTypeName(TypeElement typeElement, Processing * @return the TypeName for the builder */ public static TypeName createBuilderTypeName( - TypeElement typeElement, ProcessingContext context, String suffix) { + TypeElement typeElement, AnnotationProcessingContext context, String suffix) { String builderClassName = typeElement.getSimpleName().toString() + suffix; String packageName = context.getPackageName(typeElement); return new TypeName(packageName, builderClassName); @@ -279,22 +268,25 @@ public static TypeName createBuilderTypeName( * @param context the processing context */ private static void setBuilderAndConstructorInfo( - TypeName typeName, VariableElement param, ProcessingContext context) { + TypeName typeName, + VariableElement param, + MapperOptions options, + AnnotationProcessingContext context) { Element element = context.asElement(param.asType()); if (element instanceof TypeElement typeElement) { - setBuilderAndConstructorInfo(typeName, typeElement, context); + setBuilderAndConstructorInfo(typeName, typeElement, options, context); } } /** * Maps a list of {@code TypeMirror} to a list of simple-builder {@code TypeName}s using {@link - * #extractType(TypeMirror, ProcessingContext)}. + * #extractType(TypeMirror, MapperOptions, AnnotationProcessingContext)}. */ private static List extractTypeForList( - List typeMirrors, ProcessingContext context) { + List typeMirrors, MapperOptions options, AnnotationProcessingContext context) { List result = new ArrayList<>(typeMirrors.size()); for (TypeMirror tm : typeMirrors) { - result.add(extractType(tm, context)); + result.add(extractType(tm, options, context)); } return result; } @@ -315,18 +307,22 @@ private static List extractTypeForList( * TypeNameGeneric/rawType otherwise */ private static TypeName wrapInCollectionTypeIfApplicable( - TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { - TypeName listWrapper = tryWrapAsList(rawType, argTypes, typeMirror, context); + TypeName rawType, + List argTypes, + TypeMirror typeMirror, + MapperOptions options, + AnnotationProcessingContext context) { + TypeName listWrapper = tryWrapAsList(rawType, argTypes, typeMirror, options, context); if (listWrapper != null) { return listWrapper; } - TypeName setWrapper = tryWrapAsSet(rawType, argTypes, typeMirror, context); + TypeName setWrapper = tryWrapAsSet(rawType, argTypes, typeMirror, options, context); if (setWrapper != null) { return setWrapper; } - TypeName mapWrapper = tryWrapAsMap(rawType, argTypes, typeMirror, context); + TypeName mapWrapper = tryWrapAsMap(rawType, argTypes, typeMirror, options, context); if (mapWrapper != null) { return mapWrapper; } @@ -342,7 +338,11 @@ 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, + MapperOptions options, + AnnotationProcessingContext context) { TypeElement listElement = context.getTypeElement("java.util.List"); if (listElement == null) { return null; @@ -359,7 +359,7 @@ private static TypeName tryWrapAsList( } List interfaceTypeArgs = - extractInterfaceTypeArguments(typeMirror, listElement, context); + extractInterfaceTypeArguments(typeMirror, listElement, options, context); TypeName elementType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); return new TypeNameList(rawType, argTypes, elementType); } @@ -371,7 +371,11 @@ 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, + MapperOptions options, + AnnotationProcessingContext context) { TypeElement setElement = context.getTypeElement("java.util.Set"); if (setElement == null) { return null; @@ -388,7 +392,7 @@ private static TypeName tryWrapAsSet( } List interfaceTypeArgs = - extractInterfaceTypeArguments(typeMirror, setElement, context); + extractInterfaceTypeArguments(typeMirror, setElement, options, context); TypeName elementType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); return new TypeNameSet(rawType, argTypes, elementType); } @@ -400,7 +404,11 @@ 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, + MapperOptions options, + AnnotationProcessingContext context) { TypeElement mapElement = context.getTypeElement("java.util.Map"); if (mapElement == null) { return null; @@ -421,7 +429,7 @@ private static TypeName tryWrapAsMap( } List interfaceTypeArgs = - extractInterfaceTypeArguments(typeMirror, mapElement, context); + extractInterfaceTypeArguments(typeMirror, mapElement, options, context); TypeName keyType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); TypeName valueType = interfaceTypeArgs.size() < 2 ? null : interfaceTypeArgs.get(1); return new TypeNameMap(rawType, argTypes, keyType, valueType); @@ -436,7 +444,7 @@ private static TypeName tryWrapAsMap( * @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, AnnotationProcessingContext context) { boolean isInterface = typeElement.equals(interfaceElement); boolean hasConstructor = !isInterface @@ -469,7 +477,7 @@ private static TypeName createFallbackTypeName(TypeName rawType, List * @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, AnnotationProcessingContext context) { TypeElement parameterElement = context.getTypeElement(parameterTypeName); if (parameterElement == null) { return false; @@ -505,14 +513,17 @@ private static boolean hasConstructorWithParameterOfType( * @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, + MapperOptions options, + AnnotationProcessingContext context) { // Walk the supertype hierarchy to find the specific instantiation of the target interface TypeMirror found = findSupertype(typeMirror, targetInterface, context); if (found instanceof DeclaredType declaredType) { List typeArgs = declaredType.getTypeArguments(); if (!typeArgs.isEmpty()) { - return extractTypeForList(new ArrayList<>(typeArgs), context); + return extractTypeForList(new ArrayList<>(typeArgs), options, context); } } @@ -528,7 +539,7 @@ public static List extractInterfaceTypeArguments( * @return the matching supertype, or null if not found */ private static TypeMirror findSupertype( - TypeMirror typeMirror, TypeElement targetInterface, ProcessingContext context) { + TypeMirror typeMirror, TypeElement targetInterface, AnnotationProcessingContext context) { if (!(typeMirror instanceof DeclaredType)) { return null; } @@ -549,7 +560,8 @@ private static TypeMirror findSupertype( return null; } - private static TypeName extractType(TypeMirror typeOfParameter, ProcessingContext context) { + private static TypeName extractType( + TypeMirror typeOfParameter, MapperOptions options, AnnotationProcessingContext context) { TypeName typeName = typeOfParameter.accept( new SimpleTypeVisitor14() { @@ -587,23 +599,23 @@ public TypeName visitDeclared(DeclaredType t, Void p) { : null; if (t.getTypeArguments().isEmpty() && !(enclosing instanceof TypeNameGeneric)) { return wrapInCollectionTypeIfApplicable( - rawType, List.of(), typeOfParameter, context); + rawType, List.of(), typeOfParameter, options, context); } List typesExtracted = new ArrayList<>(t.getTypeArguments()); if (typesExtracted.isEmpty()) { return wrapInCollectionTypeIfApplicable( - rawType, List.of(), typeOfParameter, context); + rawType, List.of(), typeOfParameter, options, context); } else { - List argTypes = extractTypeForList(typesExtracted, context); + List argTypes = extractTypeForList(typesExtracted, options, context); return wrapInCollectionTypeIfApplicable( - rawType, argTypes, typeOfParameter, context); + rawType, argTypes, typeOfParameter, options, context); } } @Override public TypeNameArray visitArray(ArrayType t, Void p) { - return new TypeNameArray(extractType(t.getComponentType(), context)); + return new TypeNameArray(extractType(t.getComponentType(), options, context)); } @Override @@ -619,7 +631,7 @@ protected TypeName defaultAction(TypeMirror e, Void p) { }, null); - if (typeName != null && context.getConfiguration().shouldCopyTypeAnnotations()) { + if (typeName != null && options.copyTypeAnnotations()) { List annotations = FieldAnnotationExtractor.extractAnnotations(typeOfParameter, context); annotations.forEach(typeName::addAnnotation); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/MapperOptions.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/MapperOptions.java new file mode 100644 index 00000000..6615344d --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/MapperOptions.java @@ -0,0 +1,57 @@ +/* + * MIT License + * + * Copyright (c) 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.Optional; +import java.util.function.Function; +import javax.lang.model.element.TypeElement; +import org.javahelpers.simple.builders.processor.model.type.TypeName; + +/** + * Options controlling how {@link JavaLangMapper} maps elements to {@code TypeName} instances. + * + * @param copyTypeAnnotations whether annotations declared on a type usage are copied onto the + * mapped {@code TypeName} + * @param generatedTypeResolver resolves a source type element to the generated type usable for it + * (e.g. its builder), or empty when none applies; may be {@code null} to disable resolution + */ +public record MapperOptions( + boolean copyTypeAnnotations, Function> generatedTypeResolver) { + + /** Default options: no annotation copying, no generated-type resolution. */ + public static final MapperOptions DEFAULT = new MapperOptions(false, null); + + /** + * Resolves the generated type usable for the given type element. + * + * @param typeElement the type element to resolve + * @return the usable generated type, or empty if resolution is disabled or no match exists + */ + public Optional resolveGeneratedType(TypeElement typeElement) { + return generatedTypeResolver != null + ? generatedTypeResolver.apply(typeElement) + : Optional.empty(); + } +} 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..5fb9a706 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java @@ -0,0 +1,47 @@ +/* + * MIT License + * + * Copyright (c) 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.exceptions.BuilderException; +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 + * @throws BuilderException if there is an error in source code generation + */ + void generateClass(GenerationTargetClassDto classDef) throws BuilderException; +} 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..8cc6983d --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java @@ -0,0 +1,83 @@ +/* + * MIT License + * + * Copyright (c) 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, and the performance tracker. + */ +public class GenerationEnvironment { + + private final ProcessingEnvironment processingEnvironment; + private final ProcessingLogger logger; + private final PerformanceTracker performanceTracker; + + /** + * 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 + */ + public GenerationEnvironment( + ProcessingEnvironment processingEnvironment, + ProcessingLogger logger, + PerformanceTracker performanceTracker) { + this.processingEnvironment = processingEnvironment; + this.logger = logger; + this.performanceTracker = performanceTracker; + } + + /** + * 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 performance tracker + */ + public PerformanceTracker getPerformanceTracker() { + return performanceTracker; + } +} 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..e8baef39 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java @@ -0,0 +1,52 @@ +/* + * MIT License + * + * Copyright (c) 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/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 56d994d7..d019cda7 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,9 +24,9 @@ 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; @@ -44,6 +44,8 @@ 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.classgen.ClassCodeGenerator; +import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; @@ -60,7 +62,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; @@ -73,27 +76,42 @@ 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; + /** Logger for debug output during code generation. */ + private final ProcessingLogger logger; + + /** Performance tracker for sub-phase timing (Source Construction, File Writing). */ + private final PerformanceTracker performanceTracker; + + /** Factory for creating a source formatter per formatting mode. */ + private final RoasterSourceFormatterFactory formatterFactory; + /** Cached formatters per formatting mode (at most 3 instances, created lazily). */ private final EnumMap formatterCache = new EnumMap<>(FormattingMode.class); /** - * Creates a code generator using the given processing context and environment. + * Creates a code generator using the given generation environment and formatter factory. * - * @param context processing context providing logger, performance tracking, type lookup and - * formatter profile - * @param processingEnv processing environment providing the filer for generated source files + * @param environment the generation environment providing processing environment, logger, and + * performance tracker + * @param formatterFactory factory creating a {@link RoasterSourceFormatter} per formatting mode, + * applying any configured formatter profile */ - 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"); + public RoasterCodeGenerator( + GenerationEnvironment environment, RoasterSourceFormatterFactory formatterFactory) { + 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.formatterFactory = + Objects.requireNonNull(formatterFactory, "formatterFactory must not be null"); } /** @@ -106,7 +124,7 @@ public RoasterCodeGenerator(ProcessingContext context, ProcessingEnvironment pro * @return a cached or new formatter instance */ private RoasterSourceFormatter getFormatter(FormattingMode mode) { - return formatterCache.computeIfAbsent(mode, context::createSourceFormatter); + return formatterCache.computeIfAbsent(mode, formatterFactory::create); } /** @@ -115,42 +133,43 @@ private RoasterSourceFormatter getFormatter(FormattingMode mode) { * @param classDef DTO containing all information to create the class * @throws BuilderException if there is an error in source code generation */ + @Override public void generateClass(GenerationTargetClassDto classDef) throws BuilderException { - context.debugStartOperation( + 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(); + performanceTracker.endPhase(PHASE_STRING_GENERATION); + performanceTracker.startPhase(); sourceCode = formatSource(unformatted, classDef.getFormattingMode()); // 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); } - 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); @@ -158,24 +177,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); @@ -198,13 +217,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()); @@ -214,20 +233,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) { @@ -240,15 +259,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) { @@ -264,19 +283,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); } /** @@ -307,7 +326,7 @@ private List resolveMethodConflicts(List methods) { if (existing == null) { signatureToMethod.put(signature, method); } else { - context.warning( + logger.warning( " Unexpected duplicate method signature: '%s' — keeping first occurrence (safety net)", signature); } @@ -328,16 +347,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) { @@ -570,7 +589,7 @@ private String formatSource(String rawSource, FormattingMode mode) { private void writeClassToFile(String sourceCode, GenerationTargetClassDto classDef) throws BuilderException { - context.debug( + logger.debug( "Writing class to file: %s.%s", classDef.getTypeName().getPackageName(), classDef.getTypeName().getClassName()); @@ -614,10 +633,11 @@ private void writeClassToFile(String sourceCode, GenerationTargetClassDto classD */ private boolean builderClassAlreadyExists(String qualifiedName) { try { - TypeElement existingType = context.getTypeElement(qualifiedName); + TypeElement existingType = + processingEnv.getElementUtils().getTypeElement(qualifiedName); return existingType != null; } catch (Exception e) { - context.debug( + logger.debug( "Error checking if builder class '%s' already exists: %s", qualifiedName, StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "No message"); return false; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatterFactory.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatterFactory.java new file mode 100644 index 00000000..96cfa50a --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatterFactory.java @@ -0,0 +1,40 @@ +/* MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen.roaster; + +import org.javahelpers.simple.builders.core.enums.FormattingMode; + +/** Factory for creating a {@link RoasterSourceFormatter} per {@link FormattingMode}. */ +@FunctionalInterface +public interface RoasterSourceFormatterFactory { + + /** + * Creates a source formatter for the given formatting mode, applying any configured formatter + * profile. + * + * @param mode the formatting mode + * @return a new formatter instance + */ + RoasterSourceFormatter create(FormattingMode mode); +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java new file mode 100644 index 00000000..5605f791 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/AnnotationProcessingContext.java @@ -0,0 +1,338 @@ +/* + * MIT License + * + * Copyright (c) 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.List; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; + +/** + * Generic context object that wraps {@link Elements}, {@link Types}, and logging utilities from + * annotation processing, providing domain-specific methods for type and element operations. + * + *

This is the annotation-processor independent part of {@link ProcessingContext}: it contains no + * builder-specific configuration and can be reused by any annotation processor that works on the + * shared code-generation model. + */ +public class AnnotationProcessingContext { + private final Elements elementUtils; + private final Types typeUtils; + private final ProcessingLogger logger; + private final ProcessingEnvironment processingEnv; + private final PerformanceTracker performanceTracker; + + /** + * Creates a new annotation processing context. + * + * @param logger the logging utility for the annotation processor + * @param processingEnv the processing environment providing access to utilities and facilities + * @param performanceTracker the performance tracker for measuring processing phases + */ + public AnnotationProcessingContext( + ProcessingLogger logger, + ProcessingEnvironment processingEnv, + PerformanceTracker performanceTracker) { + this.elementUtils = processingEnv.getElementUtils(); + this.typeUtils = processingEnv.getTypeUtils(); + this.logger = logger; + this.processingEnv = processingEnv; + this.performanceTracker = performanceTracker; + } + + /** + * Gets the {@link Elements} utility of the processing environment. + * + * @return the elements utility + */ + public Elements getElementUtils() { + return elementUtils; + } + + /** + * Gets the {@link Types} utility of the processing environment. + * + * @return the types utility + */ + public Types getTypeUtils() { + return typeUtils; + } + + /** + * Gets the processing logger. + * + * @return the processing logger + */ + public ProcessingLogger getLogger() { + return logger; + } + + /** + * Gets the processing environment. + * + * @return the processing environment + */ + public ProcessingEnvironment getProcessingEnvironment() { + return processingEnv; + } + + /** + * Gets the performance tracker for this processing context. + * + * @return the performance tracker instance + */ + public PerformanceTracker getPerformanceTracker() { + return performanceTracker; + } + + /** + * 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 + */ + public TypeElement getTypeElement(String qualifiedName) { + return elementUtils.getTypeElement(qualifiedName); + } + + /** + * Get the TypeElement for a given TypeName. + * + * @param typeName the TypeName containing package and class name + * @return the type element, or null if not found + */ + public TypeElement getTypeElement(TypeName typeName) { + if (typeName == null) { + return null; + } + return getTypeElement(typeName.getFullQualifiedName()); + } + + /** + * Get the package containing an element. + * + * @param element the element + * @return the package element + */ + public PackageElement getPackageOf(Element element) { + return elementUtils.getPackageOf(element); + } + + /** + * Get the package name of an element. + * + * @param element the element + * @return the qualified package name + */ + public String getPackageName(Element element) { + return elementUtils.getPackageOf(element).getQualifiedName().toString(); + } + + /** + * Get all members of a type, including inherited members. + * + * @param typeElement the type to inspect + * @return list of all members + */ + @SuppressWarnings("java:S1452") + public List getAllMembers(TypeElement typeElement) { + return elementUtils.getAllMembers(typeElement); + } + + /** + * Get the Javadoc comment for an element. + * + * @param element the element + * @return the doc comment, or null if none + */ + public String getDocComment(Element element) { + return elementUtils.getDocComment(element); + } + + /** + * Convert a type mirror to its corresponding element. + * + * @param typeMirror the type mirror + * @return the element, or null if not representable as an element + */ + public Element asElement(TypeMirror typeMirror) { + return typeUtils.asElement(typeMirror); + } + + /** + * Check if two types are the same type. + * + * @param type1 first type + * @param type2 second type + * @return true if the types are the same + */ + public boolean isSameType(TypeMirror type1, TypeMirror type2) { + return typeUtils.isSameType(type1, type2); + } + + /** + * Get the erasure of a type (removes generic type information). + * + * @param typeMirror the type to erase + * @return the erasure of the type + */ + public TypeMirror erasure(TypeMirror typeMirror) { + return typeUtils.erasure(typeMirror); + } + + /** + * Check if one type is assignable to another. + * + * @param type1 the type to check + * @param type2 the target type + * @return true if type1 is assignable to type2 + */ + public boolean isAssignable(TypeMirror type1, TypeMirror type2) { + return typeUtils.isAssignable(type1, type2); + } + + /** + * Returns the direct supertypes of a type. + * + * @param typeMirror the type + * @return list of direct supertypes + */ + public List directSupertypes(TypeMirror typeMirror) { + return typeUtils.directSupertypes(typeMirror); + } + + /** + * Logs an info-level message that appears in normal Maven output. + * + * @param message the info message to log + */ + public void info(String message) { + logger.info(message); + } + + /** + * Logs an info-level message with a formatted string. + * + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string + */ + public void info(String format, Object... args) { + logger.info(format, args); + } + + /** + * Logs a debug message visible when debug logging is enabled. + * + * @param message the debug message to log + */ + public void debug(String message) { + logger.debug(message); + } + + /** + * Logs a debug message with a formatted string. Only visible when enabled via -Averbose=true or + * -Asimplebuilder.verbose=true. + * + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string + */ + public void debug(String format, Object... args) { + logger.debug(format, args); + } + + /** + * Starts a new hierarchical operation context for logging with formatted message. + * + * @param format the format string for the operation message + * @param args arguments referenced by the format specifiers + */ + public void debugStartOperation(String format, Object... args) { + logger.debugStartOperation(format, args); + } + + /** Ends the current hierarchical operation context for logging. */ + public void debugEndOperation() { + logger.debugEndOperation(); + } + + /** Ends the current hierarchical operation context with a closing message for logging. */ + public void debugEndOperation(String format, Object... args) { + logger.debugEndOperation(format, args); + } + + /** Resets the indentation level to prevent cascading errors between processing runs. */ + public void resetIndentation() { + logger.resetIndentation(); + } + + /** + * Logs a warning message without requiring a specific element context. + * + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string + */ + public void warning(String format, Object... args) { + logger.warning(null, format, args); + } + + /** + * Reports a warning at the location of the given element with a formatted message. + * + * @param element the element where the warning occurred, used for location information + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string + */ + public void warning(Element element, String format, Object... args) { + logger.warning(element, format, args); + } + + /** + * Reports an error with a formatted message. + * + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string + */ + public void error(String format, Object... args) { + logger.error(format, args); + } + + /** + * Reports an error at the location of the given element with a formatted message. + * + * @param element the element where the error occurred, used for location information + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string + */ + public void error(Element element, String format, Object... args) { + logger.error(element, format, args); + } +} 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 26e4b395..6ed7fa51 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 @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; @@ -99,6 +100,55 @@ public BuilderConfiguration getGlobalConfiguration() { return globalConfiguration; } + /** + * Reads a complete BuilderConfiguration from compiler arguments. + * + *

This method reads all configuration options from compiler arguments like: + * + *

+ * + *

Options set via {@code @SimpleBuilder.Options} on the annotated type are not handled here; + * they are read by {@link #resolveConfiguration(Element)} 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: + * + *

    + *
  1. {@code CompilerArgumentsEnum} — add the enum constant with its applier. + *
  2. {@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}. + *
  3. {@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 obtain raw option values from + * @param logger the logger for debug output + * @return a BuilderConfiguration with values read from compiler arguments + */ + public static BuilderConfiguration readBuilderConfiguration( + CompilerArgumentsReader reader, ProcessingLogger logger) { + BuilderConfiguration.Builder builder = BuilderConfiguration.builder(); + Stream.of(CompilerArgumentsEnum.values()) + .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 fc1b80cc..c74a161b 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,11 +46,13 @@ 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; import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; +import org.javahelpers.simple.builders.processor.analysis.MapperOptions; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; @@ -449,7 +451,8 @@ private static BuilderDefinitionDto initializeBuilderDefinition( "Builder will be generated as: %s.%s", packageName, simpleClassName + builderSuffix); // Extract generics from the annotated type via mapper (stream-based) - JavaLangMapper.map2GenericParameterDtos(annotatedType, context).forEach(result::addGeneric); + JavaLangMapper.map2GenericParameterDtos(annotatedType, mapperOptions(context), context) + .forEach(result::addGeneric); return result; } @@ -469,7 +472,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( @@ -634,7 +638,7 @@ private static Optional createFieldFromSetter( VariableElement fieldParameter = parameters.get(0); TypeElement dtoTypeElement = (TypeElement) mth.getEnclosingElement(); - TypeName dtoType = JavaLangMapper.map2TypeName(dtoTypeElement, context); + TypeName dtoType = JavaLangMapper.map2TypeName(dtoTypeElement, mapperOptions(context), context); // Extract only the @param Javadoc for the single setter parameter (if present) String fullJavaDoc = context.getDocComment(mth); @@ -688,7 +692,7 @@ private static Optional createFieldFromConstructor( JavaLangAnalyser.extractParamJavaDoc(context.getDocComment(annotatedType), param); // Convert TypeElement to TypeName once - TypeName dtoType = JavaLangMapper.map2TypeName(annotatedType, context); + TypeName dtoType = JavaLangMapper.map2TypeName(annotatedType, mapperOptions(context), context); // Check for field name conflicts and rename if necessary String finalFieldName = resolveFieldNameConflict(fieldName, param, fieldNameRegistry, context); @@ -729,7 +733,7 @@ private static String resolveFieldNameConflict( } // Conflict detected: rename the new field by appending the simple type name - MethodParameterDto paramDto = map2MethodParameter(param, context); + MethodParameterDto paramDto = map2MethodParameter(param, mapperOptions(context), context); if (paramDto == null) { // If we can't determine the type, just return the original name return fieldName; @@ -781,7 +785,7 @@ private static Optional createFieldDto( TypeName dtoType, TypeName builderType, ProcessingContext context) { - MethodParameterDto paramDto = map2MethodParameter(param, context); + MethodParameterDto paramDto = map2MethodParameter(param, mapperOptions(context), context); if (paramDto == null || dtoType == null) { return Optional.empty(); } @@ -1155,9 +1159,24 @@ private static void applyDeprecationSuppressions( * @param context processing context * @return {@code true} if the selected constructor is deprecated */ + /** + * Builds the {@link MapperOptions} for type mapping from the processing context: annotation + * copying follows the resolved configuration and generated-type resolution delegates to the + * builder scope resolver. + * + * @param context the processing context + * @return the mapper options for this processing round + */ + private static MapperOptions mapperOptions(ProcessingContext context) { + return new MapperOptions( + context.getConfiguration().shouldCopyTypeAnnotations(), + context.getBuilderScopeResolver()::resolveUsableBuilderType); + } + 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 7e89eb14..9b0baa82 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)), @@ -255,6 +255,7 @@ public enum CompilerArgumentsEnum { * * @return The option name */ + @Override public String getOptionName() { return optionName; } @@ -266,6 +267,7 @@ public String getOptionName() { * * @return The full compiler argument */ + @Override public String getCompilerArgument() { return OPTION_PREFIX + optionName; } 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 8ad1efbb..5573c021 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 @@ -24,18 +24,14 @@ package org.javahelpers.simple.builders.processor.processing; -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; @@ -58,10 +54,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 argument to read * @return the value of the compiler argument, or null if not set */ - public String readValue(CompilerArgumentsEnum argument) { + public String readValue(CompilerOption argument) { // Try the -D JVM system property first (e.g., -Dsimplebuilder.verbose) String value = System.getProperty(argument.getCompilerArgument()); @@ -81,59 +77,13 @@ public String readValue(CompilerArgumentsEnum argument) { /** * Reads the value of a compiler argument as a boolean. * - *

Returns true if the value equals "true" (case-insensitive), false otherwise. + *

Returns true if the value equals "true" or "enabled" (case-insensitive), false otherwise. * - * @param argument the compiler argument enum to read - * @return true if the value is "true" (case-insensitive), false otherwise + * @param argument the compiler argument to read + * @return true if the value is "true" or "enabled" (case-insensitive), false otherwise */ - public boolean readBooleanValue(CompilerArgumentsEnum argument) { + public boolean readBooleanValue(CompilerOption argument) { String value = readValue(argument); return Strings.CI.equalsAny(value, "true", "enabled"); } - - /** - * 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: - * - *

    - *
  1. {@code CompilerArgumentsEnum} — add the enum constant with its applier. - *
  2. {@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}. - *
  3. {@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()) - .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..4fbcb91c --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerOption.java @@ -0,0 +1,49 @@ +/* + * MIT License + * + * Copyright (c) 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/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java index 414a364b..ecce0f4b 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 @@ -1,5 +1,4 @@ -/* - * MIT License +/* MIT License * * Copyright (c) 2026 Andreas Igel * @@ -24,39 +23,26 @@ package org.javahelpers.simple.builders.processor.processing; -import java.util.List; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; -import javax.lang.model.element.PackageElement; -import javax.lang.model.element.TypeElement; -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.generators.registry.GeneratorRegistry; import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; -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; import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker; import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; /** - * Context object that wraps Elements, Types, and logging utilities from annotation processing, - * providing domain-specific methods for type and element operations. - * - *

This eliminates repetitive parameter passing and provides a cleaner API. + * Builder-specific processing context. Adds builder configuration, the generator registry and the + * builder scope resolver on top of the generic element/type/log utilities of {@link + * AnnotationProcessingContext}. */ -public final class ProcessingContext { - private final Elements elementUtils; - private final Types typeUtils; - private final ProcessingLogger logger; +public final class ProcessingContext extends AnnotationProcessingContext { private final BuilderConfigurationReader configurationReader; - private final ProcessingEnvironment processingEnv; - private final PerformanceTracker performanceTracker; private final String formatterProfile; private final BuilderScopeResolver builderScopeResolver; private GeneratorRegistry generatorRegistry; @@ -73,25 +59,33 @@ public ProcessingContext( ProcessingLogger logger, BuilderConfiguration globalConfiguration, ProcessingEnvironment processingEnv) { - this.elementUtils = processingEnv.getElementUtils(); - this.typeUtils = processingEnv.getTypeUtils(); - this.logger = logger; - this.processingEnv = processingEnv; + super(logger, processingEnv, createPerformanceTracker(processingEnv)); this.configurationReader = - new BuilderConfigurationReader(globalConfiguration, logger, elementUtils); - // Initialize performance tracker based on compiler argument + new BuilderConfigurationReader(globalConfiguration, logger, getElementUtils()); + this.formatterProfile = + StringUtils.trimToNull( + new CompilerArgumentsReader(processingEnv) + .readValue(CompilerArgumentsEnum.FORMATTER_PROFILE)); + this.builderScopeResolver = new BuilderScopeResolver(this); + // GeneratorRegistry will be lazily initialized on first access + } + + /** + * Creates the performance tracker based on the builder compiler arguments. + * + * @param processingEnv the processing environment providing compiler options + * @return an {@link ActivePerformanceTracker} when enabled, a {@link NoOpPerformanceTracker} + * otherwise + */ + private static PerformanceTracker createPerformanceTracker(ProcessingEnvironment processingEnv) { CompilerArgumentsReader argReader = new CompilerArgumentsReader(processingEnv); 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 NoOpPerformanceTracker(); - this.builderScopeResolver = new BuilderScopeResolver(this); - // GeneratorRegistry will be lazily initialized on first access + return perfTrackingEnabled + ? new ActivePerformanceTracker( + perfOutputFile, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN) + : new NoOpPerformanceTracker(); } /** @@ -131,41 +125,11 @@ public BuilderConfigurationReader getConfigurationReader() { */ public GeneratorRegistry getGeneratorRegistry() { if (generatorRegistry == null) { - generatorRegistry = new GeneratorRegistry(this, processingEnv); + generatorRegistry = new GeneratorRegistry(this, getProcessingEnvironment()); } return generatorRegistry; } - /** - * Gets the performance tracker for this processing context. - * - *

When performance tracking is disabled, returns a {@link NoOpPerformanceTracker} that has - * zero overhead. When enabled via {@code -Asimplebuilder.performanceTracking=true}, returns an - * {@link ActivePerformanceTracker} that measures execution times. - * - * @return the performance tracker instance - */ - public PerformanceTracker getPerformanceTracker() { - return performanceTracker; - } - - /** - * Starts a new performance tracking phase. The phase name is passed to {@link - * #endPerformancePhase(String)} for recording. - */ - public void startPerformancePhase() { - performanceTracker.startPhase(); - } - - /** - * Ends the current performance tracking phase and records its duration under the given name. - * - * @param phase the phase name to record - */ - 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. @@ -174,7 +138,7 @@ public void endPerformancePhase(String phase) { * @return a new formatter instance */ public RoasterSourceFormatter createSourceFormatter(FormattingMode mode) { - return new RoasterSourceFormatter(logger, mode, formatterProfile); + return new RoasterSourceFormatter(getLogger(), mode, formatterProfile); } /** @@ -189,217 +153,6 @@ public BuilderScopeResolver getBuilderScopeResolver() { return builderScopeResolver; } - /** - * 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 - */ - public TypeElement getTypeElement(String qualifiedName) { - return elementUtils.getTypeElement(qualifiedName); - } - - /** - * Get the TypeElement for a given TypeName. - * - * @param typeName the TypeName containing package and class name - * @return the type element, or null if not found - */ - public TypeElement getTypeElement(TypeName typeName) { - if (typeName == null) { - return null; - } - return getTypeElement(typeName.getFullQualifiedName()); - } - - /** - * Get the package containing an element. - * - * @param element the element - * @return the package element - */ - public PackageElement getPackageOf(Element element) { - return elementUtils.getPackageOf(element); - } - - /** - * Get the package name of an element. - * - * @param element the element - * @return the qualified package name - */ - public String getPackageName(Element element) { - return elementUtils.getPackageOf(element).getQualifiedName().toString(); - } - - /** - * Get all members of a type, including inherited members. - * - * @param typeElement the type to inspect - * @return list of all members - */ - @SuppressWarnings("java:S1452") - public List getAllMembers(TypeElement typeElement) { - return elementUtils.getAllMembers(typeElement); - } - - /** - * Get the Javadoc comment for an element. - * - * @param element the element - * @return the doc comment, or null if none - */ - public String getDocComment(Element element) { - return elementUtils.getDocComment(element); - } - - /** - * Convert a type mirror to its corresponding element. - * - * @param typeMirror the type mirror - * @return the element, or null if not representable as an element - */ - public Element asElement(TypeMirror typeMirror) { - return typeUtils.asElement(typeMirror); - } - - /** - * Check if two types are the same type. - * - * @param type1 first type - * @param type2 second type - * @return true if the types are the same - */ - public boolean isSameType(TypeMirror type1, TypeMirror type2) { - return typeUtils.isSameType(type1, type2); - } - - /** - * Get the erasure of a type (removes generic type information). - * - * @param typeMirror the type to erase - * @return the erasure of the type - */ - public TypeMirror erasure(TypeMirror typeMirror) { - return typeUtils.erasure(typeMirror); - } - - /** - * Check if one type is assignable to another. - * - * @param type1 the type to check - * @param type2 the target type - * @return true if type1 is assignable to type2 - */ - public boolean isAssignable(TypeMirror type1, TypeMirror type2) { - return typeUtils.isAssignable(type1, type2); - } - - /** - * Returns the direct supertypes of a type. - * - * @param typeMirror the type - * @return list of direct supertypes - */ - public java.util.List directSupertypes(TypeMirror typeMirror) { - return typeUtils.directSupertypes(typeMirror); - } - - /** - * Logs an info-level message that appears in normal Maven output. - * - * @param message the info message to log - */ - public void info(String message) { - logger.info(message); - } - - /** - * Logs an info-level message with a formatted string. - * - * @param format the format string - * @param args arguments referenced by the format specifiers in the format string - */ - public void info(String format, Object... args) { - logger.info(format, args); - } - - /** - * Logs a debug message visible when Maven is run with -X flag. - * - * @param message the debug message to log - */ - public void debug(String message) { - logger.debug(message); - } - - /** - * Logs a debug message with a formatted string. Only visible when enabled via -Averbose=true or - * -Asimplebuilder.verbose=true. - * - * @param format the format string - * @param args arguments referenced by the format specifiers in the format string - */ - public void debug(String format, Object... args) { - logger.debug(format, args); - } - - /** - * Starts a new hierarchical operation context for logging with formatted message. - * - * @param format the format string for the operation message - * @param args arguments referenced by the format specifiers - */ - public void debugStartOperation(String format, Object... args) { - logger.debugStartOperation(format, args); - } - - /** Ends the current hierarchical operation context for logging. */ - public void debugEndOperation() { - logger.debugEndOperation(); - } - - /** Ends the current hierarchical operation context with a closing message for logging. */ - public void debugEndOperation(String format, Object... args) { - logger.debugEndOperation(format, args); - } - - /** Resets the indentation level to prevent cascading errors between processing runs. */ - public void resetIndentation() { - logger.resetIndentation(); - } - - /** - * Logs a warning message without requiring a specific element context. - * - * @param format the format string - * @param args arguments referenced by the format specifiers in the format string - */ - public void warning(String format, Object... args) { - logger.warning(null, format, args); - } - - /** - * Reports a warning at the location of the given element with a formatted message. - * - * @param element the element where the warning occurred, used for location information - * @param format the format string - * @param args arguments referenced by the format specifiers in the format string - */ - public void warning(Element element, String format, Object... args) { - logger.warning(element, format, args); - } - - /** - * Reports an error with a formatted message. - * - * @param format the format string - * @param args arguments referenced by the format specifiers in the format string - */ - public void error(String format, Object... args) { - logger.error(format, args); - } - /** * Returns whether strict/fail-fast generation mode is enabled via the global compiler * configuration. @@ -420,9 +173,9 @@ public boolean isStrictModeEnabled() { */ public void reportBasedOnStrictMode(Element element, String format, Object... args) { if (isStrictModeEnabled()) { - logger.error(element, format, args); + error(element, format, args); } else { - logger.warning(element, format, args); + warning(element, format, args); } } @@ -434,9 +187,9 @@ public void reportBasedOnStrictMode(Element element, String format, Object... ar */ public void reportBasedOnStrictMode(String format, Object... args) { if (isStrictModeEnabled()) { - logger.error(format, args); + error(format, args); } else { - logger.warning(format, args); + warning(format, args); } } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingPhases.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingPhases.java new file mode 100644 index 00000000..49c7ce11 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingPhases.java @@ -0,0 +1,79 @@ +/* + * MIT License + * + * Copyright (c) 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; + +/** + * 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 ProcessingPhases { + + // 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. */ + public 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. */ + public static final Map> PHASE_CHILDREN = phaseChildren(); + + 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 ProcessingPhases() {} +} 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 840cefcb..4a500395 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 (with a phase hierarchy supplied at construction for + * report display), 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,36 +64,13 @@ */ 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 for report display, in display order. */ + private final List topLevelPhases; + + /** Phase hierarchy for report display: parent phase to ordered child phases. */ + private final Map> phaseChildren; private final Map phaseTimes = new ConcurrentHashMap<>(); private final Map generatorTimes = new ConcurrentHashMap<>(); @@ -121,8 +98,22 @@ public final class ActivePerformanceTracker implements PerformanceTracker { * @param outputFilePath optional path for JSON report output; null or empty disables file output */ public ActivePerformanceTracker(String outputFilePath) { + this(outputFilePath, List.of(), Map.of()); + } + + /** + * Creates a new ActivePerformanceTracker with a phase hierarchy for report display. + * + * @param outputFilePath optional path for JSON report output; null or empty disables file output + * @param topLevelPhases top-level phases in display order + * @param phaseChildren parent phase to ordered child phases + */ + public ActivePerformanceTracker( + String outputFilePath, List topLevelPhases, Map> phaseChildren) { this.totalStartTime = System.nanoTime(); this.outputFilePath = outputFilePath; + this.topLevelPhases = topLevelPhases; + this.phaseChildren = phaseChildren; } @Override @@ -211,9 +202,8 @@ public void generateReport(ProcessingLogger logger) { // Phase breakdown (hierarchical, using hardcoded 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); + for (int i = 0; i < topLevelPhases.size(); i++) { + reportPhase(logger, topLevelPhases.get(i), totalSeconds, "", i == topLevelPhases.size() - 1); } logger.info(""); @@ -356,7 +346,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 = phaseChildren.get(phase); if (children != null) { String childPrefix = prefix + (isLast ? " " : "│ "); for (int i = 0; i < children.size(); i++) { @@ -388,7 +378,7 @@ private void writeJsonReport(long totalNanos) throws IOException { // Phase breakdown (hierarchical) Map phaseBreakdown = new LinkedHashMap<>(); - for (String phase : TOP_LEVEL_PHASES) { + for (String phase : topLevelPhases) { phaseBreakdown.put(phase, buildPhaseJson(phase, totalNanos)); } root.put("phaseBreakdown", phaseBreakdown); @@ -435,7 +425,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 = phaseChildren.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 508dbd53..b385854b 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/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java index fa3bf1a5..7ddf2838 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 @@ -48,18 +46,17 @@ public class ProcessingLogger { /** * 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 + * used to report errors, warnings, and other notices during annotation processing. * * @param processingEnv the processing environment providing messager and options + * @param debugEnabled whether debug-level messages are emitted */ - public ProcessingLogger(ProcessingEnvironment processingEnv) { + public ProcessingLogger(ProcessingEnvironment processingEnv, boolean debugEnabled) { // 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.debugEnabled = debugEnabled; } /** Resets ThreadLocal state to ensure clean state between test runs. */ 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 c7bcd27c..a1e361af 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 @@ -261,7 +261,9 @@ public boolean process(Set annotations, RoundEnvironment TypeElement helper = processingEnv.getElementUtils().getTypeElement("lib.LibHelper"); ProcessingContext context = new ProcessingContext( - new ProcessingLogger(processingEnv), BuilderConfiguration.DEFAULT, processingEnv); + new ProcessingLogger(processingEnv, false), + BuilderConfiguration.DEFAULT, + processingEnv); context.initConfigurationForProcessingTarget(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 212e7fce..55f99b9d 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; @@ -204,7 +205,8 @@ void readBuilderConfiguration_NoArguments_ReturnsDefaults() { CompilerArgumentsReader reader = new CompilerArgumentsReader(env); BuilderConfiguration config = - reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger()); + BuilderConfigurationReader.readBuilderConfiguration( + reader, CapturingProcessingLogger.create().logger()); assertNotNull(config, "Configuration should not be null"); assertEquals(OptionState.UNSET, config.generateFieldSupplier()); @@ -245,7 +247,8 @@ void readBuilderConfiguration_AllOptionsSet_ReadsCorrectly() { CompilerArgumentsReader reader = new CompilerArgumentsReader(env); BuilderConfiguration config = - reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger()); + BuilderConfigurationReader.readBuilderConfiguration( + reader, CapturingProcessingLogger.create().logger()); assertEquals(OptionState.ENABLED, config.generateFieldSupplier()); assertEquals(OptionState.DISABLED, config.generateFieldConsumer()); @@ -274,7 +277,8 @@ void readBuilderConfiguration_MixedValidInvalid_HandlesGracefully() { CompilerArgumentsReader reader = new CompilerArgumentsReader(env); BuilderConfiguration config = - reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger()); + BuilderConfigurationReader.readBuilderConfiguration( + reader, CapturingProcessingLogger.create().logger()); assertEquals( OptionState.UNSET, config.generateFieldSupplier(), "Invalid option should be UNSET"); @@ -299,7 +303,8 @@ void readBuilderConfiguration_EmptyStringValues_HandlesGracefully() { CompilerArgumentsReader reader = new CompilerArgumentsReader(env); BuilderConfiguration config = - reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger()); + BuilderConfigurationReader.readBuilderConfiguration( + 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 fd729e32..247ed7ca 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 @@ -29,6 +29,7 @@ import javax.annotation.processing.ProcessingEnvironment; import org.javahelpers.simple.builders.core.enums.AccessModifier; +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; @@ -76,9 +77,12 @@ void shouldWrapRenderingRuntimeExceptionInBuilderException() { classDef.addConstructor(constructor); ProcessingEnvironment env = ProcessingEnvironmentStub.createEmpty(); - ProcessingLogger logger = new ProcessingLogger(env); + ProcessingLogger logger = new ProcessingLogger(env, false); 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::createSourceFormatter); BuilderException thrown = assertThrows(BuilderException.class, () -> generator.generateClass(classDef)); 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 208b2c06..b99ad897 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.ProcessingPhases; import org.javahelpers.simple.builders.processor.testing.CapturingProcessingLogger; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -47,10 +49,12 @@ 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, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startClass("TestClassA"); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_BUILDER_DEFINITION_EXTRACTION); + tracker.endPhase(ProcessingPhases.PHASE_BUILDER_DEFINITION_EXTRACTION); tracker.startGenerator(); tracker.endGenerator("FieldSupplierGenerator"); tracker.startEnhancer(); @@ -59,7 +63,7 @@ private JsonNode generateReportAndParseJson(String outputFile) throws IOExceptio tracker.startClass("TestClassB"); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_DTO_MAPPING); + tracker.endPhase(ProcessingPhases.PHASE_DTO_MAPPING); tracker.endClass(3, 1); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); @@ -72,7 +76,9 @@ private JsonNode generateReportAndParseJson(String outputFile) throws IOExceptio @Test void generateReport_withNoData_logsBasicReport() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -85,7 +91,9 @@ void generateReport_withNoData_logsBasicReport() { @Test void generateReport_withClassData_logsClassCount() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.endClass(4, 1); @@ -100,7 +108,9 @@ void generateReport_withClassData_logsClassCount() { @Test void generateReport_withGeneratorData_logsGeneratorStats() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.startGenerator(); tracker.endGenerator("MyGenerator"); @@ -116,7 +126,9 @@ void generateReport_withGeneratorData_logsGeneratorStats() { @Test void generateReport_withEnhancerData_logsEnhancerStats() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.startEnhancer(); tracker.endEnhancer("MyEnhancer"); @@ -132,11 +144,13 @@ void generateReport_withEnhancerData_logsEnhancerStats() { @Test void generateReport_withPhaseData_logsPhaseBreakdown() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION); + tracker.endPhase(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_CODE_GENERATION); + tracker.endPhase(ProcessingPhases.PHASE_CODE_GENERATION); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -145,17 +159,19 @@ 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(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION))); assertTrue( capturing.messages().stream() - .anyMatch(m -> m.contains(PerformanceTracker.PHASE_CODE_GENERATION))); + .anyMatch(m -> m.contains(ProcessingPhases.PHASE_CODE_GENERATION))); } @Test void generateReport_withElementCollectionPhase_logsPhaseInReport() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_ELEMENT_COLLECTION); + tracker.endPhase(ProcessingPhases.PHASE_ELEMENT_COLLECTION); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -163,16 +179,20 @@ 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(ProcessingPhases.PHASE_ELEMENT_COLLECTION)), + "Text report should contain " + ProcessingPhases.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(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_ELEMENT_COLLECTION); + tracker.endPhase(ProcessingPhases.PHASE_ELEMENT_COLLECTION); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -181,9 +201,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(ProcessingPhases.PHASE_ELEMENT_COLLECTION), + "JSON phaseBreakdown should contain " + ProcessingPhases.PHASE_ELEMENT_COLLECTION); + JsonNode elementCollection = phases.get(ProcessingPhases.PHASE_ELEMENT_COLLECTION); assertTrue(elementCollection.has("elapsedNanos")); assertTrue(elementCollection.has("elapsedSeconds")); assertTrue(elementCollection.has("percentage")); @@ -191,7 +211,9 @@ void jsonReport_phaseBreakdown_containsElementCollectionPhase() throws IOExcepti @Test void generateReport_withNullOutputFile_doesNotWriteFile() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.endClass(1, 0); @@ -273,12 +295,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(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION)); + assertTrue(phases.has(ProcessingPhases.PHASE_BUILDER_DEFINITION_EXTRACTION)); + assertTrue(phases.has(ProcessingPhases.PHASE_DTO_MAPPING)); + assertTrue(phases.has(ProcessingPhases.PHASE_CODE_GENERATION)); - JsonNode codeGen = phases.get(PerformanceTracker.PHASE_CODE_GENERATION); + JsonNode codeGen = phases.get(ProcessingPhases.PHASE_CODE_GENERATION); assertTrue(codeGen.has("elapsedNanos")); assertTrue(codeGen.has("elapsedSeconds")); assertTrue(codeGen.has("percentage")); @@ -289,17 +311,21 @@ 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(ProcessingPhases.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(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -326,8 +352,10 @@ 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, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); + tracker.endPhase(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION); tracker.endGenerator("NonexistentGenerator"); tracker.endEnhancer("NonexistentEnhancer"); tracker.endClass(5, 2); @@ -346,7 +374,11 @@ 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(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.startGenerator(); tracker.endGenerator("GenA"); @@ -371,7 +403,11 @@ 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(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.startGenerator(); tracker.endGenerator("Gen\"\\\n\t\r\b\fA\u0001"); @@ -392,11 +428,15 @@ 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(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION); + tracker.endPhase(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION); + tracker.endPhase(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -404,14 +444,17 @@ void multiplePhases_accumulateTime() throws IOException { JsonNode root = new ObjectMapper().readTree(Files.readString(jsonFile)); JsonNode phase = - root.get("phaseBreakdown").get(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION); + root.get("phaseBreakdown").get(ProcessingPhases.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", + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.endClass(1, 0); @@ -446,7 +489,11 @@ 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(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); 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 0f5cab07..e01938fb 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,19 @@ private CapturingProcessingLogger(List messages, ProcessingLogger logger public static CapturingProcessingLogger create() { List messages = new ArrayList<>(); ProcessingLogger logger = - new ProcessingLogger(createCapturingEnvironment(messages, Collections.emptyMap())); + new ProcessingLogger(createCapturingEnvironment(messages, Collections.emptyMap()), false); 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"))); + new ProcessingLogger(createCapturingEnvironment(messages, Collections.emptyMap()), true); return new CapturingProcessingLogger(messages, logger); } From 4dde491ab8b86b427d620b2b31299852181c37dc Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:23:05 +0000 Subject: [PATCH 2/2] style: apply google-java-format to RoasterCodeGenerator Co-Authored-By: Andreas Igel --- .../processor/classgen/roaster/RoasterCodeGenerator.java | 3 +-- 1 file changed, 1 insertion(+), 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 d019cda7..3a97cf2f 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 @@ -633,8 +633,7 @@ private void writeClassToFile(String sourceCode, GenerationTargetClassDto classD */ private boolean builderClassAlreadyExists(String qualifiedName) { try { - TypeElement existingType = - processingEnv.getElementUtils().getTypeElement(qualifiedName); + TypeElement existingType = processingEnv.getElementUtils().getTypeElement(qualifiedName); return existingType != null; } catch (Exception e) { logger.debug(