From 3c750be9def7ecdb8ac70d02a8a5a8f0b87d317d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:34:46 +0000 Subject: [PATCH 1/6] processor: decouple codegen layer from builder internals (phase 0) --- .../builders/processor/BuilderProcessor.java | 84 +++++-- .../analysis/FieldAnnotationExtractor.java | 13 +- .../processor/analysis/JavaLangAnalyser.java | 27 ++- .../processor/analysis/JavaLangMapper.java | 117 +++++----- .../analysis/TypeMappingContext.java | 144 ++++++++++++ .../classgen/ClassCodeGenerator.java | 48 ++++ .../classgen/CodeGenerationException.java | 54 +++++ .../classgen/GenerationEnvironment.java | 97 +++++++++ .../classgen/GenerationIssueReporter.java | 42 ++++ .../processor/classgen/GenerationPhases.java | 52 +++++ .../processor/classgen/SourceFormatter.java | 45 ++++ .../roaster/RoasterCodeGenerator.java | 205 ++++++++---------- .../roaster/RoasterSourceFormatter.java | 19 +- .../classgen/roaster/package-info.java | 4 +- .../BuilderConfigurationReader.java | 52 +++++ .../processing/BuilderDefinitionCreator.java | 7 +- .../processing/CompilerArgumentsEnum.java | 2 +- .../processing/CompilerArgumentsReader.java | 72 +----- .../processor/processing/CompilerOption.java | 49 +++++ .../processor/processing/PipelinePhases.java | 84 +++++++ .../processing/ProcessingContext.java | 65 ++++-- .../processing/SourceFormatterResolver.java | 72 ++++++ .../logging/ActivePerformanceTracker.java | 59 ++--- .../logging/PerformanceTracker.java | 25 --- .../processing/logging/PhaseHierarchy.java | 51 +++++ .../processing/logging/ProcessingLogger.java | 56 +++-- .../processor/BuilderScopeResolverTest.java | 2 +- .../CompilerArgumentsReaderTest.java | 29 ++- .../RoasterCodeGeneratorResilienceTest.java | 44 +++- .../roaster/RoasterSourceFormatterTest.java | 18 +- .../logging/ActivePerformanceTrackerTest.java | 101 +++++---- .../testing/CapturingProcessingLogger.java | 9 +- 32 files changed, 1285 insertions(+), 463 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/CodeGenerationException.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationIssueReporter.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/SourceFormatter.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerOption.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/processing/PipelinePhases.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/processing/SourceFormatterResolver.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PhaseHierarchy.java diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java index 16363245..0c560e70 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java @@ -25,11 +25,11 @@ package org.javahelpers.simple.builders.processor; import static org.javahelpers.simple.builders.processor.processing.BuilderDefinitionCreator.extractFromElement; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_BUILDER_DEFINITION_EXTRACTION; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_CODE_GENERATION; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_DTO_MAPPING; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_ELEMENT_COLLECTION; +import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_BUILDER_DEFINITION_EXTRACTION; +import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_CODE_GENERATION; +import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_CONFIGURATION_RESOLUTION; +import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_DTO_MAPPING; +import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_ELEMENT_COLLECTION; import com.google.auto.service.AutoService; import java.util.ArrayList; @@ -63,6 +63,9 @@ import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver; import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; +import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator; +import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException; +import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment; import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator; @@ -79,6 +82,7 @@ import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; import org.javahelpers.simple.builders.processor.processing.ProcessingTarget; +import org.javahelpers.simple.builders.processor.processing.SourceFormatterResolver; import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker; import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; @@ -95,25 +99,37 @@ public class BuilderProcessor extends AbstractProcessor { private ProcessingContext context; private ProcessingLogger logger; - private RoasterCodeGenerator codeGenerator; + private ClassCodeGenerator codeGenerator; + private SourceFormatterResolver formatterResolver; private JacksonModuleGenerator jacksonModuleGenerator; private boolean supportedJdk = true; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - this.logger = new ProcessingLogger(processingEnv); - logger.debug("Starting BuilderProcessor..."); - // Read global configuration from compiler arguments CompilerArgumentsReader reader = new CompilerArgumentsReader(processingEnv); - BuilderConfiguration globalConfig = reader.readBuilderConfiguration(logger); + this.logger = + reader.readBooleanValue(CompilerArgumentsEnum.VERBOSE) + ? ProcessingLogger.createVerbose(processingEnv) + : ProcessingLogger.create(processingEnv); + logger.debug("Starting BuilderProcessor..."); + + BuilderConfiguration globalConfig = + BuilderConfigurationReader.readCompilerConfiguration(reader, logger); logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig); SimpleBuildersSpiIntegration.initCompilation(processingEnv.getElementUtils()); this.context = new ProcessingContext(logger, globalConfig, processingEnv); - this.codeGenerator = new RoasterCodeGenerator(context, processingEnv); + this.codeGenerator = + new RoasterCodeGenerator( + new GenerationEnvironment( + processingEnv, + logger, + context.getPerformanceTracker(), + context::reportBasedOnStrictMode)); + this.formatterResolver = new SourceFormatterResolver(processingEnv, logger); this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig); // Initialize GeneratorRegistry once during processor initialization @@ -222,8 +238,10 @@ private void generateJacksonModules(PerformanceTracker tracker) { String packageName = moduleClassDef.getTypeName().getPackageName(); context.info("Generating Jackson Module in package '%s'", packageName); try { - codeGenerator.generateClass(moduleClassDef); - } catch (BuilderException e) { + assertTargetClassDoesNotExist(moduleClassDef); + codeGenerator.generateClass( + moduleClassDef, formatterResolver.resolve(moduleClassDef.getFormattingMode())); + } catch (CodeGenerationException e) { // By default Jackson module generation failures are warnings. In opt-in strict mode // they are promoted to errors that fail the build. context.reportBasedOnStrictMode( @@ -592,6 +610,38 @@ public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } + /** + * Fails the build with a descriptive error when the class to be generated already exists (a + * manually written class or a leftover from a previous compilation). + * + * @param classDef the class definition to check + * @throws CodeGenerationException if a class with the target qualified name already exists + */ + private void assertTargetClassDoesNotExist(GenerationTargetClassDto classDef) + throws CodeGenerationException { + String qualifiedName = classDef.getTypeName().getFullQualifiedName(); + boolean exists; + try { + exists = context.getTypeElement(qualifiedName) != null; + } catch (Exception e) { + context.debug( + "Error checking if class '%s' already exists: %s", + qualifiedName, e.getMessage() != null ? e.getMessage() : "No message"); + exists = false; + } + if (exists) { + throw new CodeGenerationException( + """ + Builder class '%s' already exists. This may be a manually written builder or a previously generated builder. + To resolve this: + 1. If you have a manual builder, consider renaming it or removing @SimpleBuilder from the DTO + 2. If this is from a previous compilation, clean and rebuild the project + 3. Check that you're not trying to generate multiple builders for the same DTO + """ + .formatted(qualifiedName)); + } + } + private void process(Element annotatedElement, BuilderConfiguration config, String builderPackage) throws BuilderException { context.initProcessingTarget(new ProcessingTarget(config, builderPackage)); @@ -622,7 +672,13 @@ private void process(Element annotatedElement, BuilderConfiguration config, Stri // Track Code Generation (parent phase; sub-phases tracked inside RoasterCodeGenerator) tracker.startPhase(); - codeGenerator.generateClass(renderingDto); + try { + assertTargetClassDoesNotExist(renderingDto); + codeGenerator.generateClass( + renderingDto, formatterResolver.resolve(renderingDto.getFormattingMode())); + } catch (CodeGenerationException e) { + throw new BuilderException(annotatedElement, e); + } tracker.endPhase(PHASE_CODE_GENERATION); // Collect info for Jackson Module if enabled diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java index 22796f60..b83c7686 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java @@ -40,7 +40,6 @@ import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; -import org.javahelpers.simple.builders.processor.processing.ProcessingContext; /** Extractor for field annotations, converting them from Java model elements to DTOs. */ public final class FieldAnnotationExtractor { @@ -82,7 +81,7 @@ private FieldAnnotationExtractor() { * Extracts the {@code @Deprecated} annotation from the given element, preserving its members * (e.g. {@code since} and {@code forRemoval}). * - *

Unlike {@link #extractAnnotations(VariableElement, ProcessingContext)}, this method targets + *

Unlike {@link #extractAnnotations(VariableElement, TypeMappingContext)}, this method targets * a single annotation and works on any {@link Element} (method, field, parameter, record * component, type), not just {@link VariableElement}s. * @@ -92,7 +91,7 @@ private FieldAnnotationExtractor() { * element is not deprecated */ public static Optional extractDeprecatedAnnotation( - Element element, ProcessingContext context) { + Element element, TypeMappingContext context) { if (element == null) { return Optional.empty(); } @@ -120,7 +119,7 @@ public static Optional extractDeprecatedAnnotation( * @return list of annotations to be copied to the builder field */ public static List extractAnnotations( - VariableElement param, ProcessingContext context) { + VariableElement param, TypeMappingContext context) { List annotations = new ArrayList<>(); List annotationMirrors = param.getAnnotationMirrors(); @@ -140,7 +139,7 @@ public static List extractAnnotations( * @return list of annotations to be copied to the builder field */ public static List extractAnnotations( - javax.lang.model.type.TypeMirror typeMirror, ProcessingContext context) { + javax.lang.model.type.TypeMirror typeMirror, TypeMappingContext context) { List annotations = new ArrayList<>(); List annotationMirrors = typeMirror.getAnnotationMirrors(); @@ -160,7 +159,7 @@ public static List extractAnnotations( * @return Optional containing the extracted annotation, or empty if it should be skipped */ private static Optional extractAnnotation( - AnnotationMirror mirror, ProcessingContext context) { + AnnotationMirror mirror, TypeMappingContext context) { // Get the annotation type element Element annotationElement = mirror.getAnnotationType().asElement(); if (!(annotationElement instanceof TypeElement annotationType)) { @@ -188,7 +187,7 @@ private static Optional extractAnnotation( * @return Optional containing the extracted annotation */ private static Optional extractAnnotationWithoutFiltering( - AnnotationMirror mirror, ProcessingContext context) { + AnnotationMirror mirror, TypeMappingContext context) { Element annotationElement = mirror.getAnnotationType().asElement(); if (!(annotationElement instanceof TypeElement annotationType)) { return Optional.empty(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java index 95a584b7..15e812cf 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java @@ -49,7 +49,6 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; -import org.javahelpers.simple.builders.core.annotations.SimpleBuilderConstructor; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -204,9 +203,9 @@ public static boolean isSetterForField(ExecutableElement mth) { * @param context processing context * @return {@code true}, if the element has an accessible empty constructor */ - public static boolean hasEmptyConstructor(TypeElement typeElement, ProcessingContext context) { + public static boolean hasEmptyConstructor(TypeElement typeElement, TypeMappingContext context) { return ElementFilter.constructorsIn(context.getAllMembers(typeElement)).stream() - .filter(context::isMemberAccessibleFromBuilderPackage) + .filter(context::isAccessibleFromGeneratedPackage) .anyMatch(JavaLangAnalyser::hasNoParameters); } @@ -389,7 +388,7 @@ private static List findMethods( return List.of(); } return ElementFilter.methodsIn(context.getAllMembers(type)).stream() - .filter(context::isMemberAccessibleFromBuilderPackage) + .filter(context::isAccessibleFromGeneratedPackage) .filter(method -> returnTypeMatcher.test(method.getReturnType())) .filter(method -> hasParameters(method, expectedParameterTypes)) .toList(); @@ -510,7 +509,7 @@ public static List findFieldFunctions( return ElementFilter.methodsIn(context.getAllMembers(type)).stream() .filter(JavaLangAnalyser::isNotStatic) .filter(method -> isNoMethodOfObjectClass(method, context)) - .filter(context::isMemberAccessibleFromBuilderPackage) + .filter(context::isAccessibleFromGeneratedPackage) .filter(method -> method.getParameters().size() == 1) .toList(); } @@ -548,7 +547,7 @@ public static boolean hasConstructorAccepting( return false; } return ElementFilter.constructorsIn(context.getAllMembers(builderType)).stream() - .filter(context::isMemberAccessibleFromBuilderPackage) + .filter(context::isAccessibleFromGeneratedPackage) .anyMatch(constructor -> hasParameters(constructor, List.of(expectedType))); } @@ -847,24 +846,28 @@ public static Optional findSetterForField( /** * Determines which constructor to use for builder initialization. Prioritizes constructors - * annotated with {@link SimpleBuilderConstructor}. If none is annotated, selects the constructor + * annotated with the given constructor annotation. If none is annotated, selects the constructor * with the highest number of parameters. Returns empty if no constructor has parameters (i.e., * only default constructor or none found). * * @param annotatedType the type element to search for constructors + * @param constructorAnnotation the marker annotation selecting a constructor explicitly; may be + * {@code null} to skip the annotation check * @param context the processing context providing access to elements and types utilities * @return Optional containing the selected constructor, or empty if none suitable */ public static Optional findConstructorForBuilder( - TypeElement annotatedType, ProcessingContext context) { + TypeElement annotatedType, + Class constructorAnnotation, + ProcessingContext context) { List ctors = ElementFilter.constructorsIn(context.getAllMembers(annotatedType)).stream() - .filter(context::isMemberAccessibleFromBuilderPackage) + .filter(context::isAccessibleFromGeneratedPackage) .toList(); - // First, check if any constructor is annotated with @SimpleBuilderConstructor + // First, check if any constructor is annotated with the constructor annotation for (ExecutableElement ctor : ctors) { - if (ctor.getAnnotation(SimpleBuilderConstructor.class) != null) { + if (constructorAnnotation != null && ctor.getAnnotation(constructorAnnotation) != null) { return Optional.of(ctor); } } @@ -894,6 +897,6 @@ public static Optional findConstructorForBuilder( public static boolean hasAccessibleConstructor( TypeElement typeElement, ProcessingContext context) { return ElementFilter.constructorsIn(context.getAllMembers(typeElement)).stream() - .anyMatch(context::isMemberAccessibleFromBuilderPackage); + .anyMatch(context::isAccessibleFromGeneratedPackage); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java index cd15fa12..bbbe38c8 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java @@ -51,7 +51,6 @@ import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; import org.javahelpers.simple.builders.processor.model.type.TypeNameSet; import org.javahelpers.simple.builders.processor.model.type.TypeNameVariable; -import org.javahelpers.simple.builders.processor.processing.ProcessingContext; /** Helper functions to create simple builder types from java.lang types. */ public final class JavaLangMapper { @@ -74,11 +73,11 @@ public static TypeName map2TypeName(Class clazz) { * Maps the declared type parameters of the given type element into GenericParameterDto list. * * @param type the type element whose type parameters to map - * @param context the processing context + * @param context the type mapping context * @return list of GenericParameterDto representing the type parameters */ public static List map2GenericParameterDtos( - TypeElement type, ProcessingContext context) { + TypeElement type, TypeMappingContext context) { return type.getTypeParameters().stream() .map(tp -> map2GenericParameterDto(tp, context)) .toList(); @@ -88,11 +87,11 @@ public static List map2GenericParameterDtos( * Maps a single {@code TypeParameterElement} to {@code GenericParameterDto}. * * @param tp the type parameter element to map - * @param context the processing context + * @param context the type mapping context * @return a GenericParameterDto representing the type parameter */ public static GenericParameterDto map2GenericParameterDto( - TypeParameterElement tp, ProcessingContext context) { + TypeParameterElement tp, TypeMappingContext context) { GenericParameterDto g = new GenericParameterDto(); g.setName(tp.getSimpleName().toString()); tp.getBounds().stream() @@ -107,11 +106,11 @@ public static GenericParameterDto map2GenericParameterDto( * org.javahelpers.simple.builders.processor.dtos.MethodParameterDto}. * * @param param Parameter variable to be mapped - * @param context processing context + * @param context the type mapping context * @return MethodParameterDto holding the information of the param. */ public static MethodParameterDto map2MethodParameter( - VariableElement param, ProcessingContext context) { + VariableElement param, TypeMappingContext context) { MethodParameterDto result = new MethodParameterDto(); result.setParameterName(param.getSimpleName().toString()); TypeMirror typeMirror = param.asType(); @@ -134,10 +133,10 @@ public static MethodParameterDto map2MethodParameter( * without resolving builder or constructor information. * * @param typeElement the TypeElement to map - * @param context the processing context + * @param context the type mapping context * @return TypeName of the type element, or null if the type element is null */ - public static TypeName mapToTypeName(TypeElement typeElement, ProcessingContext context) { + public static TypeName mapToTypeName(TypeElement typeElement, TypeMappingContext context) { if (typeElement == null) { return null; } @@ -151,10 +150,10 @@ public static TypeName mapToTypeName(TypeElement typeElement, ProcessingContext * Maps a {@code TypeElement} to a simple-builder {@code TypeName}. * * @param typeElement the TypeElement to map - * @param context the processing context + * @param context the type mapping context * @return TypeName holding the information of the type element, or null if mapping fails */ - public static TypeName map2TypeName(TypeElement typeElement, ProcessingContext context) { + public static TypeName map2TypeName(TypeElement typeElement, TypeMappingContext context) { if (typeElement == null) { return null; } @@ -175,10 +174,10 @@ public static TypeName map2TypeName(TypeElement typeElement, ProcessingContext c * * @param typeName the TypeName to enhance with builder/constructor info * @param typeElement the TypeElement representing the type - * @param context the processing context + * @param context the type mapping context */ private static void setBuilderAndConstructorInfo( - TypeName typeName, TypeElement typeElement, ProcessingContext context) { + TypeName typeName, TypeElement typeElement, TypeMappingContext context) { setBuilderTypeIfScopeMatches(typeName, typeElement, context); setEmptyConstructorInfoIfAvailable(typeName, typeElement, context); setElementBuilderTypeIfScopeMatches(typeName, context); @@ -189,14 +188,11 @@ private static void setBuilderAndConstructorInfo( * * @param typeName the TypeName to enhance * @param typeElement the type element to check - * @param context the processing context + * @param context the type mapping context */ private static void setBuilderTypeIfScopeMatches( - TypeName typeName, TypeElement typeElement, ProcessingContext context) { - context - .getBuilderScopeResolver() - .resolveUsableBuilderType(typeElement) - .ifPresent(typeName::setResolvedBuilder); + TypeName typeName, TypeElement typeElement, TypeMappingContext context) { + context.resolveGeneratedType(typeElement).ifPresent(typeName::setResolvedBuilder); } /** @@ -204,10 +200,10 @@ private static void setBuilderTypeIfScopeMatches( * * @param typeName the TypeName to enhance * @param typeElement the type element to check - * @param context the processing context + * @param context the type mapping context */ private static void setEmptyConstructorInfoIfAvailable( - TypeName typeName, TypeElement typeElement, ProcessingContext context) { + TypeName typeName, TypeElement typeElement, TypeMappingContext context) { if (isConcreteClass(typeElement) && !TypeNameAnalyser.isJavaClass(typeName) && JavaLangAnalyser.hasEmptyConstructor(typeElement, context)) { @@ -220,10 +216,10 @@ private static void setEmptyConstructorInfoIfAvailable( * configured builder scope. * * @param typeName the TypeName to enhance - * @param context the processing context + * @param context the type mapping context */ private static void setElementBuilderTypeIfScopeMatches( - TypeName typeName, ProcessingContext context) { + TypeName typeName, TypeMappingContext context) { // Only process generic types if (!(typeName instanceof TypeNameGeneric genericType)) { return; @@ -243,10 +239,9 @@ private static void setElementBuilderTypeIfScopeMatches( return; } - // Resolve usable element builder type through the scope resolver + // Resolve usable element builder type through the generated type resolver context - .getBuilderScopeResolver() - .resolveUsableBuilderType(elementTypeElement) + .resolveGeneratedType(elementTypeElement) .ifPresent(genericType::setElementResolvedBuilder); } @@ -254,37 +249,25 @@ private static void setElementBuilderTypeIfScopeMatches( * Gets the TypeElement for a given TypeName if it exists. * * @param typeName the type name to resolve - * @param context the processing context + * @param context the type mapping context * @return the TypeElement, or null if not found or not a TypeElement */ private static TypeElement retrieveTypeElementIfExists( - TypeName typeName, ProcessingContext context) { + TypeName typeName, TypeMappingContext context) { Element element = context.getTypeElement(typeName.getFullQualifiedName()); return element instanceof TypeElement typeElement ? typeElement : null; } - /** - * Creates a TypeName for the builder of a given TypeElement using the configured builder suffix. - * - * @param typeElement the type element to create builder name for - * @param context the processing context - * @return the TypeName for the builder - */ - public static TypeName createBuilderTypeName(TypeElement typeElement, ProcessingContext context) { - return createBuilderTypeName( - typeElement, context, context.getConfiguration().getBuilderSuffix()); - } - /** * Creates a TypeName for the builder of a given TypeElement using an explicit suffix. * * @param typeElement the type element to create builder name for - * @param context the processing context + * @param context the type mapping context * @param suffix the builder class name suffix to append * @return the TypeName for the builder */ public static TypeName createBuilderTypeName( - TypeElement typeElement, ProcessingContext context, String suffix) { + TypeElement typeElement, TypeMappingContext context, String suffix) { String builderClassName = typeElement.getSimpleName().toString() + suffix; String packageName = context.getPackageName(typeElement); return new TypeName(packageName, builderClassName); @@ -295,10 +278,10 @@ public static TypeName createBuilderTypeName( * * @param typeName the TypeName to enhance with builder/constructor info * @param param the VariableElement representing the parameter - * @param context the processing context + * @param context the type mapping context */ private static void setBuilderAndConstructorInfo( - TypeName typeName, VariableElement param, ProcessingContext context) { + TypeName typeName, VariableElement param, TypeMappingContext context) { Element element = context.asElement(param.asType()); if (element instanceof TypeElement typeElement) { setBuilderAndConstructorInfo(typeName, typeElement, context); @@ -307,10 +290,10 @@ private static void setBuilderAndConstructorInfo( /** * Maps a list of {@code TypeMirror} to a list of simple-builder {@code TypeName}s using {@link - * #extractType(TypeMirror, ProcessingContext)}. + * #extractType(TypeMirror, TypeMappingContext)}. */ private static List extractTypeForList( - List typeMirrors, ProcessingContext context) { + List typeMirrors, TypeMappingContext context) { List result = new ArrayList<>(typeMirrors.size()); for (TypeMirror tm : typeMirrors) { result.add(extractType(tm, context)); @@ -329,12 +312,15 @@ private static List extractTypeForList( * @param rawType the base TypeName (preserves concrete class information) * @param argTypes the generic type arguments from the class declaration (may not match interface) * @param typeMirror the TypeMirror to check for interface implementation - * @param context the processing context + * @param context the type mapping context * @return a specialized TypeName (TypeNameList, TypeNameSet, TypeNameMap) if applicable, or a * TypeNameGeneric/rawType otherwise */ private static TypeName wrapInCollectionTypeIfApplicable( - TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { + TypeName rawType, + List argTypes, + TypeMirror typeMirror, + TypeMappingContext context) { TypeName listWrapper = tryWrapAsList(rawType, argTypes, typeMirror, context); if (listWrapper != null) { return listWrapper; @@ -361,7 +347,10 @@ private static TypeName wrapInCollectionTypeIfApplicable( * @return TypeNameList if applicable, null otherwise */ private static TypeName tryWrapAsList( - TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { + TypeName rawType, + List argTypes, + TypeMirror typeMirror, + TypeMappingContext context) { TypeElement listElement = context.getTypeElement("java.util.List"); if (listElement == null) { return null; @@ -390,7 +379,10 @@ private static TypeName tryWrapAsList( * @return TypeNameSet if applicable, null otherwise */ private static TypeName tryWrapAsSet( - TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { + TypeName rawType, + List argTypes, + TypeMirror typeMirror, + TypeMappingContext context) { TypeElement setElement = context.getTypeElement("java.util.Set"); if (setElement == null) { return null; @@ -419,7 +411,10 @@ private static TypeName tryWrapAsSet( * @return TypeNameMap if applicable, null otherwise */ private static TypeName tryWrapAsMap( - TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { + TypeName rawType, + List argTypes, + TypeMirror typeMirror, + TypeMappingContext context) { TypeElement mapElement = context.getTypeElement("java.util.Map"); if (mapElement == null) { return null; @@ -451,11 +446,11 @@ private static TypeName tryWrapAsMap( * * @param typeElement the type to check * @param interfaceElement the collection interface (List or Set) - * @param context the processing context + * @param context the type mapping context * @return true if the type is the interface itself or has a Collection constructor */ private static boolean shouldWrapAsCollectionType( - TypeElement typeElement, TypeElement interfaceElement, ProcessingContext context) { + TypeElement typeElement, TypeElement interfaceElement, TypeMappingContext context) { boolean isInterface = typeElement.equals(interfaceElement); boolean hasConstructor = !isInterface @@ -484,11 +479,11 @@ private static TypeName createFallbackTypeName(TypeName rawType, List * @param typeElement the type to check * @param parameterTypeName the fully qualified name of the parameter type (e.g., * "java.util.Collection") - * @param context the processing context + * @param context the type mapping context * @return true if the type has a constructor accepting the specified parameter type */ private static boolean hasConstructorWithParameterOfType( - TypeElement typeElement, String parameterTypeName, ProcessingContext context) { + TypeElement typeElement, String parameterTypeName, TypeMappingContext context) { TypeElement parameterElement = context.getTypeElement(parameterTypeName); if (parameterElement == null) { return false; @@ -520,11 +515,11 @@ private static boolean hasConstructorWithParameterOfType( * * @param typeMirror the type to examine * @param targetInterface the interface element (e.g., List, Set, Map) - * @param context the processing context + * @param context the type mapping context * @return list of type arguments used by the interface, or empty list if raw type */ public static List extractInterfaceTypeArguments( - TypeMirror typeMirror, TypeElement targetInterface, ProcessingContext context) { + TypeMirror typeMirror, TypeElement targetInterface, TypeMappingContext context) { // Walk the supertype hierarchy to find the specific instantiation of the target interface TypeMirror found = findSupertype(typeMirror, targetInterface, context); @@ -543,11 +538,11 @@ public static List extractInterfaceTypeArguments( * * @param typeMirror the type to search from * @param targetInterface the interface to find - * @param context the processing context + * @param context the type mapping context * @return the matching supertype, or null if not found */ private static TypeMirror findSupertype( - TypeMirror typeMirror, TypeElement targetInterface, ProcessingContext context) { + TypeMirror typeMirror, TypeElement targetInterface, TypeMappingContext context) { if (!(typeMirror instanceof DeclaredType)) { return null; } @@ -568,7 +563,7 @@ private static TypeMirror findSupertype( return null; } - private static TypeName extractType(TypeMirror typeOfParameter, ProcessingContext context) { + private static TypeName extractType(TypeMirror typeOfParameter, TypeMappingContext context) { TypeName typeName = typeOfParameter.accept( new SimpleTypeVisitor14() { @@ -638,7 +633,7 @@ protected TypeName defaultAction(TypeMirror e, Void p) { }, null); - if (typeName != null && context.getConfiguration().shouldCopyTypeAnnotations()) { + if (typeName != null && context.shouldCopyTypeAnnotations()) { List annotations = FieldAnnotationExtractor.extractAnnotations(typeOfParameter, context); annotations.forEach(typeName::addAnnotation); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java new file mode 100644 index 00000000..8d783ad4 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/TypeMappingContext.java @@ -0,0 +1,144 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.analysis; + +import java.util.List; +import java.util.Optional; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import org.javahelpers.simple.builders.processor.model.type.ResolvedBuilder; + +/** + * Everything {@link JavaLangMapper} needs for mapping elements and type mirrors to {@code TypeName} + * instances: javax.lang.model lookups plus the two mapping decisions (annotation copying and + * generated-type resolution). Implemented by the processing context; the mapper stays independent + * of builder internals. + */ +public interface TypeMappingContext { + + /** + * Resolves a type element by its fully qualified name. + * + * @param qualifiedName fully qualified type name + * @return the type element, or {@code null} if it cannot be resolved + */ + TypeElement getTypeElement(String qualifiedName); + + /** + * Returns the package name of the given element. + * + * @param element the element + * @return the qualified package name + */ + String getPackageName(Element element); + + /** + * Returns the element corresponding to a type mirror. + * + * @param typeMirror the type mirror + * @return the corresponding element + */ + Element asElement(TypeMirror typeMirror); + + /** + * Returns the erasure of a type mirror: the raw type with generic information stripped (JLS + * §4.6). A parameterized type like {@code List} erases to the raw type {@code List}, and + * a type variable erases to its leftmost bound. + * + *

Mapping uses erasure to test whether a type is some kind of a known family (e.g. {@code + * Supplier}, {@code Collection}) independent of its type arguments. + * + * @param typeMirror the type mirror + * @return the erased type + */ + TypeMirror erasure(TypeMirror typeMirror); + + /** + * Tests whether one type is assignable to another. + * + * @param type1 the first type + * @param type2 the second type + * @return {@code true} if {@code type1} is assignable to {@code type2} + */ + boolean isAssignable(TypeMirror type1, TypeMirror type2); + + /** + * Tests whether two type mirrors represent the same type. + * + * @param type1 the first type + * @param type2 the second type + * @return {@code true} if both represent the same type + */ + boolean isSameType(TypeMirror type1, TypeMirror type2); + + /** + * Returns the direct supertypes of a type mirror. + * + * @param typeMirror the type mirror + * @return list of direct supertypes + */ + List directSupertypes(TypeMirror typeMirror); + + /** + * Returns all members of a type, including inherited members. + * + * @param typeElement the type to inspect + * @return list of all member elements + */ + List getAllMembers(TypeElement typeElement); + + /** + * Tests whether a member (type, method, constructor) is accessible from the package generated + * code is emitted into. + * + * @param member the member to check + * @return {@code true} if the member is accessible from the generated package + */ + boolean isAccessibleFromGeneratedPackage(Element member); + + /** + * Logs a debug message during type mapping when debug output is enabled. + * + * @param format the message format + * @param args the format arguments + */ + void debug(String format, Object... args); + + /** + * Whether annotations declared on a type usage are copied onto the mapped {@code TypeName}. + * + * @return {@code true} if type-use annotations are copied + */ + boolean shouldCopyTypeAnnotations(); + + /** + * Resolves the generated type usable for the given type element (e.g. its builder). + * + * @param typeElement the type element to resolve + * @return the usable generated type, or empty when none applies + */ + Optional resolveGeneratedType(TypeElement typeElement); +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java new file mode 100644 index 00000000..e1357c07 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java @@ -0,0 +1,48 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen; + +import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; + +/** + * SPI for class code generators: turns a {@link GenerationTargetClassDto} into source code and + * writes it through the processing environment's filer. + * + *

Implementations are backend-specific (e.g. Roaster, JavaPoet) and are instantiated with a + * {@link GenerationEnvironment} providing the processing environment, logger, and performance + * tracker. + */ +public interface ClassCodeGenerator { + + /** + * Generates a class from the given class definition and writes the source file. + * + * @param classDef DTO containing all information to create the class + * @param formatter formats the generated source before it is written + * @throws CodeGenerationException if there is an error in source code generation + */ + void generateClass(GenerationTargetClassDto classDef, SourceFormatter formatter) + throws CodeGenerationException; +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/CodeGenerationException.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/CodeGenerationException.java new file mode 100644 index 00000000..6e1aaf5e --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/CodeGenerationException.java @@ -0,0 +1,54 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen; + +/** + * Failure of a {@link ClassCodeGenerator} to produce or write generated source code. + * + *

The exception type of the code generation layer: it intentionally knows nothing about + * builders. Callers generating builders translate it into their domain exception. + */ +public class CodeGenerationException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Creates a code generation failure with a message. + * + * @param message the failure description + */ + public CodeGenerationException(String message) { + super(message); + } + + /** + * Creates a code generation failure wrapping an underlying cause. + * + * @param cause the underlying failure + */ + public CodeGenerationException(Throwable cause) { + super(cause.getMessage(), cause); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java new file mode 100644 index 00000000..d8753a45 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java @@ -0,0 +1,97 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen; + +import javax.annotation.processing.ProcessingEnvironment; +import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; + +/** + * Environment handed to a {@link ClassCodeGenerator}: the annotation processing environment, the + * logger, the performance tracker and the reporter for generation problems. + */ +public class GenerationEnvironment { + + private final ProcessingEnvironment processingEnvironment; + private final ProcessingLogger logger; + private final PerformanceTracker performanceTracker; + private final GenerationIssueReporter issueReporter; + + /** + * Creates a new generation environment. + * + * @param processingEnvironment the annotation processing environment + * @param logger the logger for debug output during code generation + * @param performanceTracker the tracker for sub-phase timing + * @param issueReporter the reporter for problems found during generation; the reporter decides + * the severity + */ + public GenerationEnvironment( + ProcessingEnvironment processingEnvironment, + ProcessingLogger logger, + PerformanceTracker performanceTracker, + GenerationIssueReporter issueReporter) { + this.processingEnvironment = processingEnvironment; + this.logger = logger; + this.performanceTracker = performanceTracker; + this.issueReporter = issueReporter; + } + + /** + * Returns the annotation processing environment. + * + * @return the processing environment + */ + public ProcessingEnvironment getProcessingEnvironment() { + return processingEnvironment; + } + + /** + * Returns the logger for debug output during code generation. + * + * @return the logger + */ + public ProcessingLogger getLogger() { + return logger; + } + + /** + * Returns the performance tracker for sub-phase timing. + * + * @return the tracker + */ + public PerformanceTracker getPerformanceTracker() { + return performanceTracker; + } + + /** + * Returns the reporter for problems found during generation. + * + * @return the issue reporter + */ + public GenerationIssueReporter getIssueReporter() { + return issueReporter; + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationIssueReporter.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationIssueReporter.java new file mode 100644 index 00000000..ea2f947a --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationIssueReporter.java @@ -0,0 +1,42 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen; + +/** + * Reports a problem found during code generation. The receiving side decides the severity — e.g. an + * unexpected generated-code inconsistency may be a warning in normal operation and an error in a + * caller-defined strict mode. + */ +@FunctionalInterface +public interface GenerationIssueReporter { + + /** + * Reports a problem found during code generation. + * + * @param format the message format string + * @param args arguments referenced by the format specifiers in the format string + */ + void report(String format, Object... args); +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java new file mode 100644 index 00000000..0049f8f6 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java @@ -0,0 +1,52 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen; + +/** + * Phase names a {@link ClassCodeGenerator} may report through {@code PerformanceTracker.endPhase} + * while turning a class definition into a source file. + */ +public final class GenerationPhases { + + // Code Generation children + public static final String PHASE_SOURCE_CONSTRUCTION = "Source Construction"; + public static final String PHASE_FILE_WRITING = "File Writing"; + + // Source Construction children + public static final String PHASE_ELEMENT_BUILDING = "Element Building"; + public static final String PHASE_STRING_GENERATION = "String Generation"; + public static final String PHASE_FORMATTING = "Formatting"; + + // Element Building children + public static final String PHASE_CLASS_CREATION = "Class Creation"; + public static final String PHASE_CLASS_METADATA = "Class Metadata"; + public static final String PHASE_FIELDS = "Fields"; + public static final String PHASE_CONSTRUCTORS = "Constructors"; + public static final String PHASE_METHODS = "Methods"; + public static final String PHASE_NESTED_TYPES = "Nested Types"; + public static final String PHASE_CLASS_ANNOTATIONS = "Class Annotations"; + + private GenerationPhases() {} +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/SourceFormatter.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/SourceFormatter.java new file mode 100644 index 00000000..b4cf0fa2 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/SourceFormatter.java @@ -0,0 +1,45 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen; + +/** + * Formats generated source code before it is written to disk. + * + *

The generic formatting seam of the code generation layer: callers pick the formatter + * implementation matching the target's formatting mode and pass it to {@link + * ClassCodeGenerator#generateClass}. Formatting-heavy backends may implement richer formatters + * behind this interface. + */ +@FunctionalInterface +public interface SourceFormatter { + + /** + * Formats generated source code. + * + * @param rawSource the unformatted source code + * @return the formatted source code + */ + String format(String rawSource); +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 35a552cd..b37c8a71 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -24,15 +24,14 @@ package org.javahelpers.simple.builders.processor.classgen.roaster; +import static org.javahelpers.simple.builders.processor.classgen.GenerationPhases.*; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapType; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.*; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Arrays; -import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -40,13 +39,15 @@ import java.util.Set; import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.TypeElement; import javax.tools.JavaFileObject; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.core.enums.AccessModifier; -import org.javahelpers.simple.builders.core.enums.FormattingMode; -import org.javahelpers.simple.builders.processor.exceptions.BuilderException; +import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator; +import org.javahelpers.simple.builders.processor.classgen.CodeGenerationException; +import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment; +import org.javahelpers.simple.builders.processor.classgen.GenerationIssueReporter; +import org.javahelpers.simple.builders.processor.classgen.SourceFormatter; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; @@ -62,7 +63,8 @@ import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; -import org.javahelpers.simple.builders.processor.processing.ProcessingContext; +import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; import org.javahelpers.simple.builders.processor.util.ImportCollector; import org.jboss.forge.roaster.Roaster; import org.jboss.forge.roaster.model.source.AnnotationSource; @@ -75,84 +77,84 @@ import org.jboss.forge.roaster.model.source.TypeVariableSource; /** Roaster-based code generator for builder source files. */ -public class RoasterCodeGenerator { - /** Processing context providing logger, performance tracking, type lookup and profile. */ - private final ProcessingContext context; - - /** Processing environment for creating generated source files via its filer. */ +public class RoasterCodeGenerator implements ClassCodeGenerator { + /** Processing environment for accessing filer and element utilities. */ private final ProcessingEnvironment processingEnv; - /** Cached formatters per formatting mode (at most 3 instances, created lazily). */ - private final EnumMap formatterCache = - new EnumMap<>(FormattingMode.class); + /** Logger for debug output during code generation. */ + private final ProcessingLogger logger; - /** - * Creates a code generator using the given processing context and environment. - * - * @param context processing context providing logger, performance tracking, type lookup and - * formatter profile - * @param processingEnv processing environment providing the filer for generated source files - */ - public RoasterCodeGenerator(ProcessingContext context, ProcessingEnvironment processingEnv) { - this.context = Objects.requireNonNull(context, "context must not be null"); - this.processingEnv = Objects.requireNonNull(processingEnv, "processingEnv must not be null"); - } + /** Performance tracker for sub-phase timing (Source Construction, File Writing). */ + private final PerformanceTracker performanceTracker; + + /** Reporter for problems found during generation; the reporter decides the severity. */ + private final GenerationIssueReporter issueReporter; + + /** Factory for creating a source formatter per formatting mode. */ /** - * Returns a cached or newly created {@link RoasterSourceFormatter} for the given mode. + * Creates a code generator using the given generation environment. * - *

At most 3 formatter instances exist (one per {@link FormattingMode} enum value), created - * lazily on first use. - * - * @param mode the formatting mode - * @return a cached or new formatter instance + * @param environment the generation environment providing processing environment, logger, + * performance tracker and issue reporter */ - private RoasterSourceFormatter getFormatter(FormattingMode mode) { - return formatterCache.computeIfAbsent(mode, context::createSourceFormatter); + public RoasterCodeGenerator(GenerationEnvironment environment) { + this.processingEnv = + Objects.requireNonNull( + environment.getProcessingEnvironment(), "processingEnv must not be null"); + this.logger = Objects.requireNonNull(environment.getLogger(), "logger must not be null"); + this.performanceTracker = + Objects.requireNonNull( + environment.getPerformanceTracker(), "performanceTracker must not be null"); + this.issueReporter = + Objects.requireNonNull(environment.getIssueReporter(), "issueReporter must not be null"); } /** * Generates a class from the given class definition. * * @param classDef DTO containing all information to create the class - * @throws BuilderException if there is an error in source code generation + * @param formatter formats the generated source before it is written + * @throws CodeGenerationException if there is an error in source code generation */ - public void generateClass(GenerationTargetClassDto classDef) throws BuilderException { - context.debugStartOperation( + @Override + public void generateClass(GenerationTargetClassDto classDef, SourceFormatter formatter) + throws CodeGenerationException { + logger.debugStartOperation( "Code generation for class: %s", classDef.getTypeName().getClassName()); String sourceCode; try { - context.startPerformancePhase(); + performanceTracker.startPhase(); JavaClassSource source = buildClassSource(classDef); - context.startPerformancePhase(); + performanceTracker.startPhase(); String unformatted = source.toUnformattedString(); - context.endPerformancePhase(PHASE_STRING_GENERATION); - context.startPerformancePhase(); - sourceCode = formatSource(unformatted, classDef.getFormattingMode()); + performanceTracker.endPhase(PHASE_STRING_GENERATION); + performanceTracker.startPhase(); + sourceCode = formatSource(unformatted, formatter); // Roaster renders some java.lang annotations (e.g. @SuppressWarnings, @Deprecated with // members) with their FQN (@java.lang.SuppressWarnings) even though java.lang types don't // need qualification. Fix this by replacing @java.lang.Xxx with @Xxx for known annotations. sourceCode = sourceCode.replace("@java.lang.SuppressWarnings", "@SuppressWarnings"); sourceCode = sourceCode.replace("@java.lang.Deprecated", "@Deprecated"); - context.endPerformancePhase(PHASE_FORMATTING); - context.endPerformancePhase(PHASE_SOURCE_CONSTRUCTION); + performanceTracker.endPhase(PHASE_FORMATTING); + performanceTracker.endPhase(PHASE_SOURCE_CONSTRUCTION); } catch (RuntimeException ex) { - // Rendering failures (e.g. RoasterMapperException) are RuntimeExceptions. Convert them into - // a BuilderException so callers can isolate the failure to this single class and keep - // generating the remaining builders instead of aborting the whole processing round. - throw new BuilderException(null, ex); + // Rendering failures (e.g. RoasterMapperException) are RuntimeExceptions. Convert them + // into a CodeGenerationException so callers can isolate the failure to this single class and + // keep generating the remaining classes instead of aborting the whole processing round. + throw new CodeGenerationException(ex); } - context.startPerformancePhase(); + performanceTracker.startPhase(); writeClassToFile(sourceCode, classDef); - context.endPerformancePhase(PHASE_FILE_WRITING); + performanceTracker.endPhase(PHASE_FILE_WRITING); - context.debugEndOperation( + logger.debugEndOperation( "Successfully generated class: %s", classDef.getTypeName().getClassName()); } private JavaClassSource buildClassSource(GenerationTargetClassDto classDef) { - context.startPerformancePhase(); + performanceTracker.startPhase(); JavaClassSource source = createJavaClassSource(classDef); addClassMetadata(source, classDef); appendFields(source, classDef); @@ -160,24 +162,24 @@ private JavaClassSource buildClassSource(GenerationTargetClassDto classDef) { appendMethods(source, classDef); appendNestedTypes(source, classDef); applyClassAnnotations(source, classDef); - context.endPerformancePhase(PHASE_ELEMENT_BUILDING); + performanceTracker.endPhase(PHASE_ELEMENT_BUILDING); return source; } private void applyClassAnnotations(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); + performanceTracker.startPhase(); if (CollectionUtils.isNotEmpty(classDef.getClassAnnotations())) { // Adding class annotations applyAnnotations(source, classDef.getClassAnnotations()); - context.debug("Class-level annotations added"); + logger.debug("Class-level annotations added"); } - context.endPerformancePhase(PHASE_CLASS_ANNOTATIONS); + performanceTracker.endPhase(PHASE_CLASS_ANNOTATIONS); } private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) { - context.startPerformancePhase(); + performanceTracker.startPhase(); if (CollectionUtils.isNotEmpty(classDef.getGenerics())) { - context.debug("Class has %d generic type parameter(s)", classDef.getGenerics().size()); + logger.debug("Class has %d generic type parameter(s)", classDef.getGenerics().size()); } JavaClassSource source = Roaster.create(JavaClassSource.class); @@ -200,13 +202,13 @@ private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) } } - context.debug("JavaClassSource created"); - context.endPerformancePhase(PHASE_CLASS_CREATION); + logger.debug("JavaClassSource created"); + performanceTracker.endPhase(PHASE_CLASS_CREATION); return source; } private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); + performanceTracker.startPhase(); applyJavadoc(source, classDef.getClassJavadoc()); applyVisibility(source, classDef.getClassAccessModifier()); applySuperType(source, classDef.getSuperType()); @@ -216,20 +218,20 @@ private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto c source.addInterface(RoasterMapper.mapInterfaceToTypeName(interfaceName)); } - context.debug("Class metadata added"); - context.endPerformancePhase(PHASE_CLASS_METADATA); + logger.debug("Class metadata added"); + performanceTracker.endPhase(PHASE_CLASS_METADATA); } private void appendFields(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); - context.debugStartOperation("Generating %d fields", classDef.getClassFields().size()); + performanceTracker.startPhase(); + logger.debugStartOperation("Generating %d fields", classDef.getClassFields().size()); for (ClassFieldDto fieldDto : classDef.getClassFields()) { appendField(source, fieldDto); } - context.debugEndOperation("Fields added: %d fields", source.getFields().size()); - context.endPerformancePhase(PHASE_FIELDS); + logger.debugEndOperation("Fields added: %d fields", source.getFields().size()); + performanceTracker.endPhase(PHASE_FIELDS); } private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { @@ -242,15 +244,15 @@ private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { } private void appendConstructors(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); - context.debugStartOperation("Generating %d constructors", classDef.getConstructors().size()); + performanceTracker.startPhase(); + logger.debugStartOperation("Generating %d constructors", classDef.getConstructors().size()); for (ConstructorDto constructor : classDef.getConstructors()) { appendConstructor(source, constructor); } - context.debugEndOperation("Constructors added: %d", classDef.getConstructors().size()); - context.endPerformancePhase(PHASE_CONSTRUCTORS); + logger.debugEndOperation("Constructors added: %d", classDef.getConstructors().size()); + performanceTracker.endPhase(PHASE_CONSTRUCTORS); } private void appendConstructor(JavaClassSource source, ConstructorDto constructor) { @@ -266,19 +268,19 @@ private void appendConstructor(JavaClassSource source, ConstructorDto constructo } private void appendMethods(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); - context.debugStartOperation("Generating %d method candidates", classDef.getMethods().size()); + performanceTracker.startPhase(); + logger.debugStartOperation("Generating %d method candidates", classDef.getMethods().size()); // Resolve method conflicts by signature and priority List resolvedMethods = resolveMethodConflicts(classDef.getMethods()); - context.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size()); + logger.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size()); for (MethodDto methodDto : resolvedMethods) { appendMethod(source, methodDto, false, false); } - context.debugEndOperation("Methods added: %d", resolvedMethods.size()); - context.endPerformancePhase(PHASE_METHODS); + logger.debugEndOperation("Methods added: %d", resolvedMethods.size()); + performanceTracker.endPhase(PHASE_METHODS); } /** @@ -301,7 +303,7 @@ private List resolveMethodConflicts(List methods) { if (seenSignatures.add(method.getSignatureKey())) { deduplicated.add(method); } else { - context.reportBasedOnStrictMode( + issueReporter.report( "Unexpected duplicate method signature: '%s' — keeping first occurrence (safety net)", method.getSignatureKey()); } @@ -320,16 +322,16 @@ private void appendMethod( } private void appendNestedTypes(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); + performanceTracker.startPhase(); if (CollectionUtils.isNotEmpty(classDef.getNestedTypes())) { - context.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size()); + logger.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size()); for (NestedTypeDto nestedType : classDef.getNestedTypes()) { appendNestedType(source, nestedType); - context.debug("Generated nested type: %s", nestedType.getTypeName()); + logger.debug("Generated nested type: %s", nestedType.getTypeName()); } - context.debugEndOperation("Nested types added"); + logger.debugEndOperation("Nested types added"); } - context.endPerformancePhase(PHASE_NESTED_TYPES); + performanceTracker.endPhase(PHASE_NESTED_TYPES); } private void appendNestedType(JavaClassSource source, NestedTypeDto nestedType) { @@ -556,30 +558,17 @@ private void addParameter( applyAnnotations(parameter, paramDto.getAnnotations()); } - private String formatSource(String rawSource, FormattingMode mode) { - return getFormatter(mode).format(rawSource); + private String formatSource(String rawSource, SourceFormatter formatter) { + return formatter.format(rawSource); } private void writeClassToFile(String sourceCode, GenerationTargetClassDto classDef) - throws BuilderException { - context.debug( + throws CodeGenerationException { + logger.debug( "Writing class to file: %s.%s", classDef.getTypeName().getPackageName(), classDef.getTypeName().getClassName()); String qualifiedName = classDef.getTypeName().getFullQualifiedName(); - if (builderClassAlreadyExists(qualifiedName)) { - throw new BuilderException( - null, - """ - Builder class '%s' already exists. This may be a manually written builder or a previously generated builder. - To resolve this: - 1. If you have a manual builder, consider renaming it or removing @SimpleBuilder from the DTO - 2. If this is from a previous compilation, clean and rebuild the project - 3. Check that you're not trying to generate multiple builders for the same DTO - """ - .formatted(qualifiedName)); - } - try { JavaFileObject file = processingEnv.getFiler().createSourceFile(qualifiedName); try (Writer writer = file.openWriter()) { @@ -594,25 +583,7 @@ private void writeClassToFile(String sourceCode, GenerationTargetClassDto classD """ .formatted( qualifiedName, StringUtils.isNotBlank(message) ? message : "Unknown error"); - throw new BuilderException(null, errorMessage); - } - } - - /** - * Checks if a builder class already exists by attempting to find the type element. - * - * @param qualifiedName the fully qualified name of the class to check - * @return true if the class already exists, false otherwise - */ - private boolean builderClassAlreadyExists(String qualifiedName) { - try { - TypeElement existingType = context.getTypeElement(qualifiedName); - return existingType != null; - } catch (Exception e) { - context.debug( - "Error checking if builder class '%s' already exists: %s", - qualifiedName, StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "No message"); - return false; + throw new CodeGenerationException(errorMessage); } } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatter.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatter.java index dfde5b76..f0720e82 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatter.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatter.java @@ -33,6 +33,7 @@ import java.util.Properties; import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.core.enums.FormattingMode; +import org.javahelpers.simple.builders.processor.classgen.SourceFormatter; import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; import org.jboss.forge.roaster.Roaster; import org.jboss.forge.roaster.model.util.FormatterProfileReader; @@ -50,15 +51,15 @@ * *

The Eclipse formatter profile can be overridden with a file system path or classpath resource. */ -public final class RoasterSourceFormatter { +public final class RoasterSourceFormatter implements SourceFormatter { static final String DEFAULT_FORMATTER_PROFILE_RESOURCE = "eclipse-java-format.xml"; private static final int SPACES_PER_TAB = 2; private final ProcessingLogger logger; private final FormattingMode formattingMode; - private final Properties formatterProperties; private final String formatterProfileResource; + private Properties formatterProperties; /** * Creates a formatter instance. @@ -86,7 +87,13 @@ public RoasterSourceFormatter( this.formattingMode = Objects.requireNonNull(formattingMode, "formattingMode must not be null"); this.formatterProfileResource = StringUtils.defaultIfBlank(formatterProfile, DEFAULT_FORMATTER_PROFILE_RESOURCE); - this.formatterProperties = initializeFormatterProperties(formattingMode); + } + + private Properties formatterProperties() { + if (formatterProperties == null) { + formatterProperties = initializeFormatterProperties(formattingMode); + } + return formatterProperties; } /** @@ -126,17 +133,19 @@ private Properties loadJdtFormatterProperties() { * @param rawSource the unformatted Java source code from Roaster's {@code toUnformattedString()} * @return the formatted source code */ + @Override public String format(String rawSource) { + Properties properties = formatterProperties(); if (formattingMode == FormattingMode.NONE) { return rawSource; } if (formattingMode == FormattingMode.LIGHTWEIGHT) { return lightweightFormat(rawSource); } - if (formatterProperties.isEmpty()) { + if (properties.isEmpty()) { return lightweightFormat(rawSource); } - return Roaster.format(formatterProperties, rawSource); + return Roaster.format(properties, rawSource); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java index c989db64..18fddb94 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java @@ -39,8 +39,8 @@ *

Exception classes are organized in separate packages: * *