diff --git a/.sonarlint/connectedMode.json b/.sonarlint/connectedMode.json new file mode 100644 index 00000000..09f72fd3 --- /dev/null +++ b/.sonarlint/connectedMode.json @@ -0,0 +1,5 @@ +{ + "sonarCloudOrganization": "java-helpers", + "projectKey": "java-helpers_simple-builders", + "region": "EU" +} \ No newline at end of file diff --git a/README.md b/README.md index 67765c7e..6b74fc7e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [![Java Version](https://img.shields.io/badge/Java-17%2B-blue)](https://www.oracle.com/java/technologies/javase/17-relnote-issues.html) [![Maven](https://img.shields.io/badge/Maven-3.8%2B-orange)](https://maven.apache.org/) [![codecov](https://codecov.io/gh/java-helpers/simple-builders/graph/badge.svg)](https://codecov.io/gh/java-helpers/simple-builders) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=java-helpers_simple-builders&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=java-helpers_simple-builders) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=java-helpers_simple-builders&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=java-helpers_simple-builders) ## Table of Contents - [What is Simple Builders?](#what-is-simple-builders) diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashSetBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashSetBuilder.java index 0cc44af9..57448b23 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashSetBuilder.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashSetBuilder.java @@ -67,6 +67,6 @@ public HashSetBuilder addAll(Set elements) { * @return a new HashSet containing all elements added to this builder */ public Set build() { - return new HashSet(mSet); + return new HashSet<>(mSet); } } 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 941acebd..d1b38b24 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 @@ -29,8 +29,6 @@ import com.google.auto.service.AutoService; import java.util.Set; import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.Filer; -import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; @@ -38,12 +36,11 @@ import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; -import javax.lang.model.util.Elements; -import javax.lang.model.util.Types; -import javax.tools.Diagnostic; import org.javahelpers.simple.builders.processor.dtos.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.util.JavaCodeGenerator; +import org.javahelpers.simple.builders.processor.util.ProcessingContext; +import org.javahelpers.simple.builders.processor.util.ProcessingLogger; /** * BuilderProcessor is an annotation processor for execution in generate-sources phase. The @@ -53,30 +50,26 @@ @AutoService(Processor.class) @SupportedAnnotationTypes("org.javahelpers.simple.builders.core.annotations.SimpleBuilder") public class BuilderProcessor extends AbstractProcessor { - private Types typeUtils; - private Elements elementUtils; - private Filer filer; - private Messager messager; + private ProcessingContext context; private JavaCodeGenerator codeGenerator; private boolean supportedJdk = true; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - this.typeUtils = processingEnv.getTypeUtils(); - this.elementUtils = processingEnv.getElementUtils(); - this.filer = processingEnv.getFiler(); - this.messager = processingEnv.getMessager(); - this.codeGenerator = new JavaCodeGenerator(filer); + ProcessingLogger logger = new ProcessingLogger(processingEnv.getMessager()); + this.context = + new ProcessingContext( + processingEnv.getElementUtils(), processingEnv.getTypeUtils(), logger); + this.codeGenerator = new JavaCodeGenerator(processingEnv.getFiler()); // Enforce minimum Java version (17+) for the processor SourceVersion current = processingEnv.getSourceVersion(); this.supportedJdk = isAtLeastJava17(current); if (!this.supportedJdk) { - messager.printMessage( - Diagnostic.Kind.ERROR, + context.error( "simple-builders requires Java 17 or higher for annotation processing. Detected: " - + String.valueOf(current) + + current + ". Please upgrade to JDK 17+ or disable the processor."); } } @@ -85,24 +78,24 @@ public synchronized void init(ProcessingEnvironment processingEnv) { public boolean process(Set annotations, RoundEnvironment roundEnv) { if (!supportedJdk) { // Fail fast: we already emitted an error in init(); do not attempt any processing. - return true; + return false; } // Resolve annotation as TypeElement to support environments where the Class overload // of getElementsAnnotatedWith is unavailable. TypeElement simpleBuilderAnnotation = - elementUtils.getTypeElement( + context.getTypeElement( org.javahelpers.simple.builders.core.annotations.SimpleBuilder.class .getCanonicalName()); if (simpleBuilderAnnotation == null) { - // TODO: Logging - // Annotation type not on classpath; nothing to do this round. - return true; + context.error( + "Annotation org.javahelpers.simple.builders.core.annotations.SimpleBuilder is not on classpath. So nothing to do here."); + return false; } for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(simpleBuilderAnnotation)) { try { process(annotatedElement); } catch (BuilderException ex) { - // TODO Logging + context.error(annotatedElement, "Failed to process annotated element: " + ex.getMessage()); } } return true; @@ -114,7 +107,7 @@ public SourceVersion getSupportedSourceVersion() { } private void process(Element annotatedElement) throws BuilderException { - BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, elementUtils, typeUtils); + BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, context); codeGenerator.generateBuilder(builderDef); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodCodeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodCodeDto.java index 6279c88b..c765d0bd 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodCodeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodCodeDto.java @@ -33,7 +33,7 @@ public class MethodCodeDto { private String codeFormat; /** List of placeholders in CodeFormat. Containing dynamic values too. */ - private final List codeArguments = new ArrayList<>(); + private final List> codeArguments = new ArrayList<>(); /** * Setting format of code. @@ -78,7 +78,8 @@ public String getCodeFormat() { * * @return argument values */ - public List getCodeArguments() { + @SuppressWarnings("java:S1452") + public List> getCodeArguments() { return codeArguments; } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodCodePlaceholder.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodCodePlaceholder.java index 096ee5fa..3d4d069c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodCodePlaceholder.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodCodePlaceholder.java @@ -39,7 +39,7 @@ public abstract class MethodCodePlaceholder { * @param label name of placeholder * @param value dynamic value for placeholder */ - public MethodCodePlaceholder(String label, T value) { + protected MethodCodePlaceholder(String label, T value) { this.label = label; this.value = value; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/exceptions/BuilderException.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/exceptions/BuilderException.java index c4eb2036..e7ed1a0a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/exceptions/BuilderException.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/exceptions/BuilderException.java @@ -28,7 +28,7 @@ /** Special exception for errors in processing of annotations. */ public class BuilderException extends Exception { - private final Element element; + private final transient Element element; /** * Creating an exception with location element, message and parameters. diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 301ca5ce..6df46c67 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -40,10 +40,9 @@ import javax.lang.model.element.*; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; -import javax.lang.model.util.Elements; -import javax.lang.model.util.Types; 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.IgnoreInBuilder; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.core.builders.ArrayListBuilder; @@ -57,44 +56,55 @@ public class BuilderDefinitionCreator { private static final String BUILDER_SUFFIX = "Builder"; + // Template argument keys for code generation + private static final String ARG_FIELD_NAME = "fieldName"; + private static final String ARG_DTO_METHOD_PARAM = "dtoMethodParam"; + private static final String ARG_DTO_METHOD_PARAMS = "dtoMethodParams"; + private static final String ARG_BUILDER_FIELD_WRAPPER = "builderFieldWrapper"; + private static final String ARG_HELPER_TYPE = "helperType"; + + // Parameter name suffixes + private static final String SUFFIX_CONSUMER = "Consumer"; + private static final String SUFFIX_SUPPLIER = "Supplier"; + + private BuilderDefinitionCreator() { + // Private constructor to prevent instantiation + } + /** * Extracting definition for a builder from annotated element. * * @param annotatedElement annotated elment which is target of builder creation - * @param elementUtils {@code javax.lang.model.util.Elements} utils - * @param typeUtils {@code javax.lang.model.util.Types} utils + * @param context processing context with element and type utilities * @return definition of builder * @throws BuilderException if validation or generation failed */ public static BuilderDefinitionDto extractFromElement( - Element annotatedElement, Elements elementUtils, Types typeUtils) throws BuilderException { + Element annotatedElement, ProcessingContext context) throws BuilderException { validateAnnotatedElement(annotatedElement); TypeElement annotatedType = (TypeElement) annotatedElement; BuilderDefinitionDto result = new BuilderDefinitionDto(); - String packageName = elementUtils.getPackageOf(annotatedType).getQualifiedName().toString(); + String packageName = context.getPackageName(annotatedType); String simpleClassName = annotatedType.getSimpleName().toString(); result.setBuilderTypeName(new TypeName(packageName, simpleClassName + BUILDER_SUFFIX)); result.setBuildingTargetTypeName(new TypeName(packageName, simpleClassName)); // Extract generics from the annotated type via mapper (stream-based) - JavaLangMapper.map2GenericParameterDtos(annotatedType, elementUtils, typeUtils) - .forEach(result::addGeneric); + JavaLangMapper.map2GenericParameterDtos(annotatedType, context).forEach(result::addGeneric); // Extract constructors and their parameters (treat as constructor fields) - Optional constructorOpt = - findConstructorForBuilder(annotatedType, elementUtils); + Optional constructorOpt = findConstructorForBuilder(annotatedType, context); if (constructorOpt.isPresent()) { ExecutableElement ctor = constructorOpt.get(); for (VariableElement param : ctor.getParameters()) { Optional fieldFromCtor = - createFieldFromConstructor(annotatedType, param, elementUtils, typeUtils); + createFieldFromConstructor(annotatedType, param, context); fieldFromCtor.ifPresent(result::addFieldInConstructor); } } - // Todo: moving into helper function - List allMembers = elementUtils.getAllMembers(annotatedType); + List allMembers = context.getAllMembers(annotatedType); List methods = ElementFilter.methodsIn(allMembers); // Build a set of constructor field names to avoid duplicates from setters @@ -107,7 +117,7 @@ public static BuilderDefinitionDto extractFromElement( // nur public if (isMethodRelevantForBuilder(mth)) { // Avoid adding setter-derived fields that duplicate constructor params - Optional maybeField = createFieldFromSetter(mth, elementUtils, typeUtils); + Optional maybeField = createFieldFromSetter(mth, context); if (maybeField.isPresent()) { FieldDto field = maybeField.get(); if (!ctorFieldNames.contains(field.getFieldName())) { @@ -132,16 +142,14 @@ && isNotPrivate(mth) private static void addAdditionalHelperMethodsForField( FieldDto result, String fieldName, TypeName fieldType) { - List innerTypes; - int innerTypesCnt; - if (fieldType instanceof TypeNameGeneric fieldTypeGeneric) { - innerTypes = fieldTypeGeneric.getInnerTypeArguments(); - innerTypesCnt = innerTypes.size(); - } else { - innerTypes = null; - innerTypesCnt = 0; + // Only process generic types (List, Set, Map, etc.) + if (!(fieldType instanceof TypeNameGeneric fieldTypeGeneric)) { + return; } + List innerTypes = fieldTypeGeneric.getInnerTypeArguments(); + int innerTypesCnt = innerTypes.size(); + if (isList(fieldType) && innerTypesCnt == 1) { result.addMethod( createFieldSetterWithTransform( @@ -166,8 +174,7 @@ private static void addConsumerMethodsForField( TypeName fieldType, VariableElement fieldParameter, TypeElement fieldTypeElement, - Elements elementUtils, - Types typeUtils) { + ProcessingContext context) { // Do not generate supplier methods for generic type variables (e.g., T) if (fieldType instanceof TypeNameVariable) { return; @@ -176,7 +183,7 @@ private static void addConsumerMethodsForField( if (isFunctionalInterface(fieldTypeElement)) { return; } - Optional builderTypeOpt = findBuilderType(fieldParameter, elementUtils, typeUtils); + Optional builderTypeOpt = findBuilderType(fieldParameter, context); if (builderTypeOpt.isPresent()) { TypeName builderType = builderTypeOpt.get(); result.addMethod( @@ -185,7 +192,7 @@ private static void addConsumerMethodsForField( && fieldTypeElement != null && fieldTypeElement.getKind() == javax.lang.model.element.ElementKind.CLASS && !fieldTypeElement.getModifiers().contains(Modifier.ABSTRACT) - && hasEmptyConstructor(fieldTypeElement, elementUtils)) { + && hasEmptyConstructor(fieldTypeElement, context)) { // Only generate a Consumer for concrete classes with an accessible empty constructor result.addMethod(createFieldConsumer(fieldName, fieldType)); } else if (isList(fieldType) @@ -228,25 +235,27 @@ private static void addSupplierMethodsForField( } private static Optional createFieldFromSetter( - ExecutableElement mth, Elements elementUtils, Types typeUtils) { + ExecutableElement mth, ProcessingContext context) { String methodName = mth.getSimpleName().toString(); - String fieldName = StringUtils.uncapitalize(StringUtils.removeStart(methodName, "set")); + String fieldName = StringUtils.uncapitalize(Strings.CI.removeStart(methodName, "set")); FieldDto result = new FieldDto(); result.setFieldName(fieldName); List parameters = mth.getParameters(); if (parameters.size() != 1) { - // TODO: Logging - // Sollte eigentlich nie vorkommen, da das vorher raus gefiltert wurde + // Should never happen, just to be sure here + context.warning( + mth.getEnclosingElement(), + "Setter method " + methodName + " has " + parameters.size() + " parameters, expected 1"); return Optional.empty(); } VariableElement fieldParameter = parameters.get(0); TypeMirror fieldTypeMirror = fieldParameter.asType(); - Element rawElement = typeUtils.asElement(fieldTypeMirror); + Element rawElement = context.asElement(fieldTypeMirror); TypeElement fieldTypeElement = rawElement instanceof TypeElement te ? te : null; // Extract only the @param Javadoc for the single setter parameter (if present) - String fullJavaDoc = elementUtils.getDocComment(mth); + String fullJavaDoc = context.getDocComment(mth); String parameterJavaDocExtracted = JavaLangAnalyser.extractParamJavaDoc(fullJavaDoc, fieldParameter); String parameterJavaDoc = @@ -254,10 +263,9 @@ private static Optional createFieldFromSetter( result.setJavaDoc(parameterJavaDoc); // extracting type of field - MethodParameterDto fieldParameterDto = - map2MethodParameter(fieldParameter, elementUtils, typeUtils); + MethodParameterDto fieldParameterDto = map2MethodParameter(fieldParameter, context); if (fieldParameterDto == null) { - // TODO: Logging + context.warning(mth.getEnclosingElement(), "Could not extract type of field " + fieldName); return Optional.empty(); } TypeName fieldType = fieldParameterDto.getParameterType(); @@ -265,8 +273,7 @@ private static Optional createFieldFromSetter( // Determine if a corresponding getter exists on the DTO and record its name TypeElement dtoType = (TypeElement) mth.getEnclosingElement(); - JavaLangAnalyser.findGetterForField( - dtoType, fieldName, fieldTypeMirror, elementUtils, typeUtils) + JavaLangAnalyser.findGetterForField(dtoType, fieldName, fieldTypeMirror, context) .ifPresent( getterExecutable -> result.setGetterName(getterExecutable.getSimpleName().toString())); @@ -275,7 +282,9 @@ private static Optional createFieldFromSetter( // If there are field-specific generics, no field in builder could be generated for it, so it // needs to be ignored if (CollectionUtils.isNotEmpty(mth.getTypeParameters())) { - // TODO: Logging + context.warning( + mth.getEnclosingElement(), + "Field " + fieldName + " has field-specific generics, so it will be ignored"); return Optional.empty(); } @@ -284,7 +293,7 @@ private static Optional createFieldFromSetter( // add consumer/supplier generation via helpers addConsumerMethodsForField( - result, fieldName, fieldType, fieldParameter, fieldTypeElement, elementUtils, typeUtils); + result, fieldName, fieldType, fieldParameter, fieldTypeElement, context); addSupplierMethodsForField(result, fieldName, fieldType, fieldTypeElement); addAdditionalHelperMethodsForField(result, fieldName, fieldType); @@ -296,8 +305,8 @@ private static Optional createFieldFromSetter( * the constructor argument. */ private static Optional createFieldFromConstructor( - TypeElement dtoType, VariableElement param, Elements elementUtils, Types typeUtils) { - MethodParameterDto paramDto = map2MethodParameter(param, elementUtils, typeUtils); + TypeElement dtoType, VariableElement param, ProcessingContext context) { + MethodParameterDto paramDto = map2MethodParameter(param, context); if (paramDto == null) { return Optional.empty(); } @@ -309,7 +318,7 @@ private static Optional createFieldFromConstructor( // Set javadoc (default to field name if no javadoc found) ctorField.setJavaDoc(fieldName); // Find matching getter on the DTO type - JavaLangAnalyser.findGetterForField(dtoType, fieldName, param.asType(), elementUtils, typeUtils) + JavaLangAnalyser.findGetterForField(dtoType, fieldName, param.asType(), context) .ifPresent(getter -> ctorField.setGetterName(getter.getSimpleName().toString())); // Provide a simple setter in builder to set the constructor argument ctorField.addMethod(createFieldSetterWithTransform(fieldName, null, fieldType)); @@ -337,16 +346,16 @@ private static MethodDto createFieldSetterWithTransform( this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParams:N); return this; """); - methodDto.addArgument("fieldName", fieldName); - methodDto.addArgument("dtoMethodParams", params); - methodDto.addArgument("builderFieldWrapper", TypeName.of(TrackedValue.class)); + methodDto.addArgument(ARG_FIELD_NAME, fieldName); + methodDto.addArgument(ARG_DTO_METHOD_PARAMS, params); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); return methodDto; } private static MethodDto createFieldConsumer(String fieldName, TypeName fieldType) { TypeNameGeneric consumerType = new TypeNameGeneric(map2TypeName(Consumer.class), fieldType); MethodParameterDto parameter = new MethodParameterDto(); - parameter.setParameterName(fieldName + "Consumer"); + parameter.setParameterName(fieldName + SUFFIX_CONSUMER); parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(); methodDto.setMethodName(fieldName); @@ -360,10 +369,10 @@ private static MethodDto createFieldConsumer(String fieldName, TypeName fieldTyp this.$fieldName:N = $builderFieldWrapper:T.changedValue(consumer); return this; """); - methodDto.addArgument("fieldName", fieldName); - methodDto.addArgument("dtoMethodParam", parameter.getParameterName()); - methodDto.addArgument("helperType", fieldType); - methodDto.addArgument("builderFieldWrapper", TypeName.of(TrackedValue.class)); + methodDto.addArgument(ARG_FIELD_NAME, fieldName); + methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); + methodDto.addArgument(ARG_HELPER_TYPE, fieldType); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); return methodDto; } @@ -376,7 +385,7 @@ private static MethodDto createFieldConsumerWithBuilder( private static MethodDto createFieldConsumerWithBuilder(String fieldName, TypeName builderType) { TypeNameGeneric consumerType = new TypeNameGeneric(map2TypeName(Consumer.class), builderType); MethodParameterDto parameter = new MethodParameterDto(); - parameter.setParameterName(fieldName + BUILDER_SUFFIX + "Consumer"); + parameter.setParameterName(fieldName + BUILDER_SUFFIX + SUFFIX_CONSUMER); parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(); methodDto.setMethodName(fieldName); @@ -390,17 +399,17 @@ private static MethodDto createFieldConsumerWithBuilder(String fieldName, TypeNa this.$fieldName:N = $builderFieldWrapper:T.changedValue(builder.build()); return this; """); - methodDto.addArgument("fieldName", fieldName); - methodDto.addArgument("dtoMethodParam", parameter.getParameterName()); - methodDto.addArgument("helperType", builderType); - methodDto.addArgument("builderFieldWrapper", TypeName.of(TrackedValue.class)); + methodDto.addArgument(ARG_FIELD_NAME, fieldName); + methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); + methodDto.addArgument(ARG_HELPER_TYPE, builderType); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); return methodDto; } private static MethodDto createFieldSupplier(String fieldName, TypeName fieldType) { TypeNameGeneric supplierType = new TypeNameGeneric(map2TypeName(Supplier.class), fieldType); MethodParameterDto parameter = new MethodParameterDto(); - parameter.setParameterName(fieldName + "Supplier"); + parameter.setParameterName(fieldName + SUFFIX_SUPPLIER); parameter.setParameterTypeName(supplierType); MethodDto methodDto = new MethodDto(); methodDto.setMethodName(fieldName); @@ -412,26 +421,25 @@ private static MethodDto createFieldSupplier(String fieldName, TypeName fieldTyp this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParam:N.get()); return this; """); - methodDto.addArgument("fieldName", fieldName); - methodDto.addArgument("dtoMethodParam", parameter.getParameterName()); - methodDto.addArgument("builderFieldWrapper", TypeName.of(TrackedValue.class)); + methodDto.addArgument(ARG_FIELD_NAME, fieldName); + methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); return methodDto; } private static Optional findBuilderType( - VariableElement param, Elements elementUtils, Types typeUtils) { + VariableElement param, ProcessingContext context) { TypeMirror typeOfParameter = param.asType(); if (typeOfParameter.getKind() == ARRAY || typeOfParameter.getKind().isPrimitive()) { return Optional.empty(); } - Element elementOfParameter = typeUtils.asElement(typeOfParameter); + Element elementOfParameter = context.asElement(typeOfParameter); if (elementOfParameter == null) { // Can happen for primitives or certain compiler-internal types; nothing to build return Optional.empty(); } String simpleClassName = elementOfParameter.getSimpleName().toString(); - String packageName = - elementUtils.getPackageOf(elementOfParameter).getQualifiedName().toString(); + String packageName = context.getPackageName(elementOfParameter); Optional foundBuilderAnnotation = findAnnotation(elementOfParameter, SimpleBuilder.class); boolean isClassWithoutGenerics = StringUtils.containsNone(simpleClassName, "<"); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java index 1395bca4..7e6259b5 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java @@ -38,8 +38,6 @@ import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; -import javax.lang.model.util.Elements; -import javax.lang.model.util.Types; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; import org.javahelpers.simple.builders.core.annotations.SimpleBuilderConstructor; @@ -47,6 +45,8 @@ /** Helperclass for extrating specific information from existing classes. */ public final class JavaLangAnalyser { + private static final String PARAM_TAG = "@param "; + private JavaLangAnalyser() {} /** @@ -57,8 +57,8 @@ private JavaLangAnalyser() {} */ public static boolean isNoMethodOfObjectClass(ExecutableElement mth) { String simpleNameOfParent = mth.getEnclosingElement().getSimpleName().toString(); - return !(StringUtils.equals("java.lang.Object", simpleNameOfParent) - || StringUtils.equals("Object", simpleNameOfParent)); + return !(Strings.CS.equals("java.lang.Object", simpleNameOfParent) + || Strings.CS.equals("Object", simpleNameOfParent)); } /** @@ -121,22 +121,22 @@ public static boolean isNotStatic(ExecutableElement mth) { */ public static boolean isSetterForField(ExecutableElement mth) { String name = mth.getSimpleName().toString(); - return StringUtils.length(name) > 3 - && Strings.CS.startsWith(name, "set") + return name.startsWith("set") + && StringUtils.length(name) > 3 && StringUtils.isAllUpperCase(StringUtils.substring(name, 3, 4)) && mth.getParameters().size() == 1; } /** - * Helper to check if an element has an empty Constructor. + * Check if the class (TypeElement) has an empty constructor. * * @param typeElement - * @param elementUtils + * @param context processing context * @return {@code true}, if the element has an empty constructor */ - public static boolean hasEmptyConstructor(TypeElement typeElement, Elements elementUtils) { + public static boolean hasEmptyConstructor(TypeElement typeElement, ProcessingContext context) { List constructors = - ElementFilter.constructorsIn(elementUtils.getAllMembers(typeElement)); + ElementFilter.constructorsIn(context.getAllMembers(typeElement)); return constructors.stream().anyMatch(c -> c.getParameters().isEmpty()); } @@ -209,41 +209,88 @@ public static String extractParamJavaDoc(String javaDoc, VariableElement paramet } String parameterName = parameter.getSimpleName().toString(); String[] lines = javaDoc.split("\r?\n"); - boolean capture = false; + + // Find the @param line for our parameter + int indexOfParamTag = findParamTagLine(lines, parameterName); + if (indexOfParamTag < 0) { + return null; + } + + // Extract text from the @param line and continuation lines + return extractParamText(lines, indexOfParamTag); + } + + /** + * Finds the line index containing @param tag for the given parameter name. + * + * @param lines the javadoc lines + * @param parameterName the parameter to search for + * @return the line index, or -1 if not found + */ + private static int findParamTagLine(String[] lines, String parameterName) { + for (int i = 0; i < lines.length; i++) { + String cleanedLine = cleanJavadocLine(lines[i]); + if (cleanedLine.startsWith(PARAM_TAG)) { + String rest = cleanedLine.substring(PARAM_TAG.length()).trim(); + int spaceIndex = rest.indexOf(' '); + String name = spaceIndex >= 0 ? rest.substring(0, spaceIndex) : rest; + if (Strings.CI.equals(parameterName, name)) { + return i; + } + } + } + return -1; + } + + /** + * Removes leading asterisk and whitespace from a Javadoc line. + * + * @param rawLine the raw line from Javadoc + * @return the cleaned line + */ + private static String cleanJavadocLine(String rawLine) { + String line = rawLine.trim(); + if (line.startsWith("*")) { + return line.substring(1).trim(); + } + return line; + } + + /** + * Extracts the parameter documentation text starting from the @param line. + * + * @param lines the javadoc lines + * @param startIndex the index of the @param line + * @return the extracted documentation text or null if empty + */ + private static String extractParamText(String[] lines, int startIndex) { StringBuilder sb = new StringBuilder(); - for (String rawLine : lines) { - String line = rawLine.trim(); - if (line.startsWith("*")) { - line = line.substring(1).trim(); + + // Extract initial text from the @param line + String firstLine = cleanJavadocLine(lines[startIndex]); + String rest = firstLine.substring(PARAM_TAG.length()).trim(); + int spaceIndex = rest.indexOf(' '); + if (spaceIndex >= 0) { + String initialText = rest.substring(spaceIndex + 1).trim(); + if (!initialText.isEmpty()) { + sb.append(initialText); } - if (line.startsWith("@")) { - // new tag begins; stop capturing unless this is our @param start - capture = false; - if (line.startsWith("@param ")) { - // format: @param text... - String rest = line.substring("@param ".length()).trim(); - int sp = rest.indexOf(' '); - String name = sp >= 0 ? rest.substring(0, sp) : rest; - String text = sp >= 0 ? rest.substring(sp + 1).trim() : ""; - if (Strings.CI.equals(parameterName, name)) { - if (!text.isEmpty()) { - sb.append(text); - } - capture = true; - } - } - continue; + } + + // Append continuation lines until next tag or empty line + for (int i = startIndex + 1; i < lines.length; i++) { + String cleanedLine = cleanJavadocLine(lines[i]); + + if (cleanedLine.isEmpty() || cleanedLine.startsWith("@")) { + break; } - if (capture) { - if (line.isEmpty()) { - break; - } - if (sb.length() > 0) sb.append(' '); - sb.append(line); - } else { - // TODO: add logging for not found javadoc for parameter + + if (!sb.isEmpty()) { + sb.append(' '); } + sb.append(cleanedLine); } + String result = sb.toString().trim(); return result.isEmpty() ? null : result; } @@ -257,30 +304,27 @@ public static String extractParamJavaDoc(String javaDoc, VariableElement paramet * @param dtoType the enclosing DTO type element * @param fieldName the field name (uncapitalized) * @param fieldTypeMirror the expected return type of the getter - * @param elementUtils elements utility - * @param typeUtils types utility + * @param context processing context * @return Optional containing the getter ExecutableElement if found */ public static Optional findGetterForField( TypeElement dtoType, String fieldName, TypeMirror fieldTypeMirror, - Elements elementUtils, - Types typeUtils) { + ProcessingContext context) { if (dtoType == null || fieldName == null || fieldTypeMirror == null) { return Optional.empty(); } String cap = StringUtils.capitalize(fieldName); String getterCandidate = "get" + cap; String booleanGetterCandidate = "is" + cap; - List classMethods = - ElementFilter.methodsIn(elementUtils.getAllMembers(dtoType)); + List classMethods = ElementFilter.methodsIn(context.getAllMembers(dtoType)); // Prefer boolean-style getter if present for (ExecutableElement candidate : classMethods) { String name = candidate.getSimpleName().toString(); if ((name.equals(booleanGetterCandidate) || name.equals(getterCandidate)) && candidate.getParameters().isEmpty() - && typeUtils.isSameType(candidate.getReturnType(), fieldTypeMirror)) { + && context.isSameType(candidate.getReturnType(), fieldTypeMirror)) { return Optional.of(candidate); } } @@ -298,9 +342,9 @@ public static Optional findGetterForField( * @return Optional containing the selected constructor, or empty if none suitable */ public static Optional findConstructorForBuilder( - TypeElement annotatedType, Elements elementUtils) { + TypeElement annotatedType, ProcessingContext context) { List ctors = - ElementFilter.constructorsIn(elementUtils.getAllMembers(annotatedType)); + ElementFilter.constructorsIn(context.getAllMembers(annotatedType)); // First, check if any constructor is annotated with @SimpleBuilderConstructor for (ExecutableElement ctor : ctors) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java index 68c50fe8..464b5ce4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java @@ -40,13 +40,15 @@ import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; -import javax.lang.model.util.Elements; import javax.lang.model.util.SimpleTypeVisitor14; -import javax.lang.model.util.Types; import org.javahelpers.simple.builders.processor.dtos.*; /** Helper functions to create simple builder types from java.lang types. */ public final class JavaLangMapper { + /** Private constructor to prevent instantiation of utility class. */ + private JavaLangMapper() { + // Utility class + } /** * Mapper for {@code java.util.Set} to extract the relevant @@ -77,20 +79,20 @@ public static TypeName map2TypeName(Class clazz) { /** Maps the declared type parameters of the given type element into GenericParameterDto list. */ public static List map2GenericParameterDtos( - TypeElement type, Elements elementUtils, Types typeUtils) { + TypeElement type, ProcessingContext context) { return type.getTypeParameters().stream() - .map(tp -> map2GenericParameterDto(tp, elementUtils, typeUtils)) + .map(tp -> map2GenericParameterDto(tp, context)) .toList(); } /** Maps a single {@code TypeParameterElement} to {@code GenericParameterDto}. */ public static GenericParameterDto map2GenericParameterDto( - TypeParameterElement tp, Elements elementUtils, Types typeUtils) { + TypeParameterElement tp, ProcessingContext 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, elementUtils, typeUtils)) + .map(b -> extractType(b, context)) .forEach(g::addUpperBound); return g; } @@ -100,16 +102,15 @@ public static GenericParameterDto map2GenericParameterDto( * org.javahelpers.simple.builders.processor.dtos.MethodParameterDto}. * * @param param Parameter variable to be mapped - * @param elementUtils Instance of helper for elements - * @param typeUtils Instance of helper for types + * @param context processing context * @return MethodParameterDto holding the information of the param. */ public static MethodParameterDto map2MethodParameter( - VariableElement param, Elements elementUtils, Types typeUtils) { + VariableElement param, ProcessingContext context) { MethodParameterDto result = new MethodParameterDto(); result.setParameterName(param.getSimpleName().toString()); TypeMirror typeMirror = param.asType(); - TypeName typeName = extractType(typeMirror, elementUtils, typeUtils); + TypeName typeName = extractType(typeMirror, context); if (typeName == null) { return null; } @@ -119,24 +120,23 @@ public static MethodParameterDto map2MethodParameter( /** * Maps a list of {@code TypeMirror} to a list of simple-builder {@code TypeName}s using {@link - * #extractType(TypeMirror, Elements, Types)}. + * #extractType(TypeMirror, ProcessingContext)}. */ private static List extractTypeForList( - List typeMirrors, Elements elementUtils, Types typeUtils) { + List typeMirrors, ProcessingContext context) { List result = new ArrayList<>(typeMirrors.size()); for (TypeMirror tm : typeMirrors) { - result.add(extractType(tm, elementUtils, typeUtils)); + result.add(extractType(tm, context)); } return result; } - private static TypeName extractType( - TypeMirror typeOfParameter, Elements elementUtils, Types typeUtils) { + private static TypeName extractType(TypeMirror typeOfParameter, ProcessingContext context) { return typeOfParameter.accept( new SimpleTypeVisitor14() { @Override - public TypeName visitPrimitive(PrimitiveType t, Void _p) { + public TypeName visitPrimitive(PrimitiveType t, Void p) { return switch (t.getKind()) { case BOOLEAN -> TypeNamePrimitive.BOOLEAN; case BYTE -> TypeNamePrimitive.BYTE; @@ -152,10 +152,9 @@ public TypeName visitPrimitive(PrimitiveType t, Void _p) { @Override public TypeName visitDeclared(DeclaredType t, Void p) { - TypeElement elementOfParameter = (TypeElement) typeUtils.asElement(typeOfParameter); + TypeElement elementOfParameter = (TypeElement) context.asElement(typeOfParameter); String simpleClassName = elementOfParameter.getSimpleName().toString(); - String packageName = - elementUtils.getPackageOf(elementOfParameter).getQualifiedName().toString(); + String packageName = context.getPackageName(elementOfParameter); TypeName rawType = new TypeName(packageName, simpleClassName); TypeMirror enclosingType = t.getEnclosingType(); TypeName enclosing = @@ -171,26 +170,25 @@ public TypeName visitDeclared(DeclaredType t, Void p) { if (typesExtracted.isEmpty()) { return rawType; } else { - List argTypes = extractTypeForList(typesExtracted, elementUtils, typeUtils); + List argTypes = extractTypeForList(typesExtracted, context); // Represent all generics uniformly return new TypeNameGeneric(rawType, argTypes); } } @Override - public TypeNameArray visitArray(ArrayType t, Void _p) { - return new TypeNameArray( - extractType(t.getComponentType(), elementUtils, typeUtils), false); + public TypeNameArray visitArray(ArrayType t, Void p) { + return new TypeNameArray(extractType(t.getComponentType(), context), false); } @Override - public TypeName visitTypeVariable(TypeVariable t, Void _p) { + public TypeName visitTypeVariable(TypeVariable t, Void p) { String name = t.asElement().getSimpleName().toString(); return new TypeNameVariable(name); } @Override - protected TypeName defaultAction(TypeMirror e, Void _p) { + protected TypeName defaultAction(TypeMirror e, Void p) { throw new IllegalArgumentException("Unexpected type mirror: " + e); } }, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java index 9d1a9ae6..8b2b704c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java @@ -146,7 +146,7 @@ public static CodeBlock map2CodeBlock( return CodeBlock.builder().addNamed(codeDto.getCodeFormat(), arguments).build(); } - private static Object toCodeblockValue(MethodCodePlaceholder placeHolderValue) { + private static Object toCodeblockValue(MethodCodePlaceholder placeHolderValue) { if (placeHolderValue instanceof MethodCodeStringPlaceholder stringPlaceholder) { return stringPlaceholder.getValue(); } else if (placeHolderValue instanceof MethodCodeTypePlaceholder typePlaceholder) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java new file mode 100644 index 00000000..6fe18222 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java @@ -0,0 +1,161 @@ +/* + * MIT License + * + * Copyright (c) 2025 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.util; + +import java.util.List; +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; + +/** + * 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. + */ +public final class ProcessingContext { + private final Elements elementUtils; + private final Types typeUtils; + private final ProcessingLogger logger; + + public ProcessingContext(Elements elementUtils, Types typeUtils, ProcessingLogger logger) { + this.elementUtils = elementUtils; + this.typeUtils = typeUtils; + this.logger = logger; + } + + /** + * Get a type element by its fully 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 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); + } + + /** + * Reports an error at the location of the given element. + * + * @param element the element where the error occurred + * @param message the error message + */ + public void error(Element element, String message) { + logger.error(element, message); + } + + /** + * Reports an error message. + * + * @param message the error message + */ + public void error(String message) { + logger.error(message); + } + + /** + * Logs an informational message. + * + * @param message the message to log + */ + public void log(String message) { + logger.log(message); + } + + /** + * Reports a warning at the location of the given element with the specified message. + * + * @param element the element where the warning occurred, used for location information + * @param message the warning message to be reported + */ + public void warning(Element element, String message) { + logger.warning(element, message); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java index 5b6ef3d6..65d8c417 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java @@ -58,6 +58,16 @@ public void error(Element e, String message) { messager.printMessage(Diagnostic.Kind.ERROR, message, e); } + /** + * Reports an error with the specified message. The error will be reported to the underlying + * Messager instance. + * + * @param message the error message to be reported + */ + public void error(String message) { + messager.printMessage(Diagnostic.Kind.ERROR, message); + } + /** * Posts an informational note message to the underlying Messager. This is typically used for * non-critical information during processing. @@ -67,4 +77,15 @@ public void error(Element e, String message) { public void log(String message) { messager.printMessage(Diagnostic.Kind.NOTE, message); } + + /** + * Reports a warning at the location of the given element with the specified message. The warning + * will be reported to the underlying Messager instance. + * + * @param e the element where the warning occurred, used for location information + * @param message the warning message to be reported + */ + public void warning(Element e, String message) { + messager.printMessage(Diagnostic.Kind.WARNING, message, e); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java index 10798226..7f3b9391 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java @@ -23,12 +23,14 @@ */ package org.javahelpers.simple.builders.processor.util; -import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.javahelpers.simple.builders.processor.dtos.TypeName; /** Helperclass to extract insights from TypeName. */ public class TypeNameAnalyser { + private static final String JAVA_UTIL_PACKAGE = "java.util"; + private TypeNameAnalyser() {} /** @@ -38,8 +40,8 @@ private TypeNameAnalyser() {} * @return {@code true}, if it is a {@code java.util.Map} */ public static boolean isMap(TypeName typeName) { - return StringUtils.equalsIgnoreCase(typeName.getPackageName(), "java.util") - && StringUtils.equalsIgnoreCase(typeName.getClassName(), "Map"); + return Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE) + && Strings.CI.equals(typeName.getClassName(), "Map"); } /** @@ -49,7 +51,8 @@ public static boolean isMap(TypeName typeName) { * @return {@code true}, if type is a java-base class */ public static boolean isJavaClass(TypeName typeName) { - return StringUtils.equalsAny(typeName.getPackageName(), "java.lang", "java.time", "java.util"); + return Strings.CI.equalsAny( + typeName.getPackageName(), "java.lang", "java.time", JAVA_UTIL_PACKAGE); } /** @@ -59,8 +62,8 @@ public static boolean isJavaClass(TypeName typeName) { * @return {@code true}, if it is a {@code java.util.Set} */ public static boolean isSet(TypeName typeName) { - return StringUtils.equalsIgnoreCase(typeName.getPackageName(), "java.util") - && StringUtils.equalsIgnoreCase(typeName.getClassName(), "Set"); + return Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE) + && Strings.CI.equals(typeName.getClassName(), "Set"); } /** @@ -70,7 +73,7 @@ public static boolean isSet(TypeName typeName) { * @return {@code true}, if it is a {@code java.util.List} */ public static boolean isList(TypeName typeName) { - return StringUtils.equalsIgnoreCase(typeName.getPackageName(), "java.util") - && StringUtils.equalsIgnoreCase(typeName.getClassName(), "List"); + return Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE) + && Strings.CI.equals(typeName.getClassName(), "List"); } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java index 9a9c28fa..836776af 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java @@ -1,33 +1,23 @@ package org.javahelpers.simple.builders.processor; import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.contains; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.notContains; import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; import com.google.testing.compile.Compilation; -import com.google.testing.compile.Compiler; import com.google.testing.compile.JavaFileObjects; import javax.tools.JavaFileObject; import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; /** Tests for the {@link BuilderProcessor} class. */ class BuilderProcessorTest { - protected BuilderProcessor processor; - protected Compiler compiler; - - @BeforeEach - protected void setUp() { - processor = new BuilderProcessor(); - compiler = Compiler.javac().withProcessors(processor); - } - @Test void shouldFailWhenAnnotationPlacedOnInterface() { // Given: @SimpleBuilder used on an interface instead of a class @@ -41,15 +31,12 @@ public interface WrongTargetInterface { void x(); } """); // When - Compilation compilation = - Compiler.javac().withProcessors(new BuilderProcessor()).compile(source); + Compilation compilation = compile(source); - // Then: processor swallows validation exceptions; assert no builder was generated - assertThat(compilation).succeeded(); - org.junit.jupiter.api.Assertions.assertFalse( - compilation.generatedFiles().stream() - .anyMatch(f -> f.getName().endsWith("test/WrongTargetInterfaceBuilder.java")), - "Builder should not be generated for interface target"); + // Then: compilation should fail with appropriate error message + assertThat(compilation).failed(); + assertThat(compilation) + .hadErrorContaining("The SimpleBuilder should be annotated on a class or record"); } @Test @@ -65,15 +52,11 @@ public abstract class AbstractAnnotated { } """); // When - Compilation compilation = - Compiler.javac().withProcessors(new BuilderProcessor()).compile(source); + Compilation compilation = compile(source); - // Then: processor swallows validation exceptions; assert no builder was generated - assertThat(compilation).succeeded(); - org.junit.jupiter.api.Assertions.assertFalse( - compilation.generatedFiles().stream() - .anyMatch(f -> f.getName().endsWith("test/AbstractAnnotatedBuilder.java")), - "Builder should not be generated for abstract class target"); + // Then: compilation should fail with appropriate error message + assertThat(compilation).failed(); + assertThat(compilation).hadErrorContaining("The SimpleBuilder should not be abstract"); } @Test @@ -929,7 +912,7 @@ void shouldGenerateSupplierMethodForField() { } protected Compilation compile(JavaFileObject... sourceFiles) { - return compiler.compile(sourceFiles); + return javac().withProcessors(new BuilderProcessor()).compile(sourceFiles); } @Test @@ -946,7 +929,7 @@ public class ForcedOldRelease { public ForcedOldRelease() {} } // When: compile with a lower language level to simulate older Java (no production code change) Compilation compilation = - Compiler.javac() + javac() .withProcessors(new BuilderProcessor()) .withOptions("--release", "11") .compile(source); @@ -1007,51 +990,57 @@ public void setAge(int age) { } @Test - // TODO: Replace Optional with other generic wrapper type, because for optional there is a - // feature-request void shouldHandleGenericWrapperTypes() { - JavaFileObject optionalWrapperClass = + JavaFileObject wrapperRecord = + ProcessorTestUtils.forSource( + """ + package test; + + public record WrapperRecord(T value) { + } + """); + + JavaFileObject dtoWithWrappedTypes = ProcessorTestUtils.forSource( """ package test; - import java.util.Optional; import java.time.LocalDate; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; @SimpleBuilder - public class OptionalWrapper { - private Optional name; - private Optional count; - private Optional date; + public class DtoWithWrappedTypes { + private WrapperRecord name; + private WrapperRecord count; + private WrapperRecord date; - public Optional getName() { return name; } - public void setName(Optional name) { this.name = name; } + public WrapperRecord getName() { return name; } + public void setName(WrapperRecord name) { this.name = name; } - public Optional getCount() { return count; } - public void setCount(Optional count) { this.count = count; } + public WrapperRecord getCount() { return count; } + public void setCount(WrapperRecord count) { this.count = count; } - public Optional getDate() { return date; } - public void setDate(Optional date) { this.date = date; } + public WrapperRecord getDate() { return date; } + public void setDate(WrapperRecord date) { this.date = date; } } """); // Test builder generation (without usage) - Compilation compilation = compile(optionalWrapperClass); - String optionalBuilder = loadGeneratedSource(compilation, "OptionalWrapperBuilder"); + Compilation compilation = compile(wrapperRecord, dtoWithWrappedTypes); + String optionalBuilder = loadGeneratedSource(compilation, "DtoWithWrappedTypesBuilder"); // Verify Optional wrapper generates basic methods - assertGenerationSucceeded(compilation, "OptionalWrapperBuilder", optionalBuilder); + assertGenerationSucceeded(compilation, "DtoWithWrappedTypesBuilder", optionalBuilder); ProcessorAsserts.assertContaining( optionalBuilder, - "public static OptionalWrapperBuilder create()", - "public OptionalWrapperBuilder name(Optional name)", - "public OptionalWrapperBuilder count(Optional count)", - "public OptionalWrapperBuilder date(Optional date)", - "public OptionalWrapper build()"); + "public static DtoWithWrappedTypesBuilder create()", + "public DtoWithWrappedTypesBuilder name(WrapperRecord name)", + "public DtoWithWrappedTypesBuilder count(WrapperRecord count)", + "public DtoWithWrappedTypesBuilder date(WrapperRecord date)", + "public DtoWithWrappedTypes build()"); // Verify no additional helper methods are generated for these generic types - // (since they don't match List/Set/Map patterns) + // (since they don't match List/Set/Map/Optional patterns) ProcessorAsserts.assertNotContaining( optionalBuilder, "nameBuilderConsumer", @@ -1063,9 +1052,9 @@ public class OptionalWrapper { // Backing fields present ProcessorAsserts.assertContaining( optionalBuilder, - "private TrackedValue> name = unsetValue();", - "private TrackedValue> count = unsetValue();", - "private TrackedValue> date = unsetValue();"); + "private TrackedValue> name = unsetValue();", + "private TrackedValue> count = unsetValue();", + "private TrackedValue> date = unsetValue();"); } @Test @@ -2191,7 +2180,13 @@ public void setFieldWithOwnGeneric(F fieldWithOwnGeneri // Then String generatedCode = loadGeneratedSource(compilation, builderClassName); - assertThat(compilation).succeededWithoutWarnings(); + assertThat(compilation).succeeded(); + assertThat(compilation) + .hadWarningContaining( + "Field fieldWithOwnGeneric has field-specific generics, so it will be ignored"); + assertThat(compilation) + .hadWarningContaining( + "Field fieldWithExtendedClassGeneric has field-specific generics, so it will be ignored"); ProcessorAsserts.assertingResult( generatedCode, // name field should be present @@ -2228,12 +2223,10 @@ public class InnerClass { // When Compilation compilation = compile(source); - // Then: compilation succeeds and no builder class with the conflicting name is generated - assertThat(compilation).succeeded(); - org.junit.jupiter.api.Assertions.assertFalse( - compilation.generatedFiles().stream() - .anyMatch(f -> f.getName().endsWith("test/HasInnerClassWithBuilderBuilder.java")), - "Builder should not be generated when nested builder interface is present"); + // Then: compilation should fail with appropriate error message + assertThat(compilation).failed(); + assertThat(compilation) + .hadErrorContaining("The SimpleBuilder should be declared on a top-level class only"); } @Test diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JavaLangAnalyserTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JavaLangAnalyserTest.java index b9893872..27a10a2f 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/JavaLangAnalyserTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JavaLangAnalyserTest.java @@ -5,6 +5,7 @@ import java.lang.annotation.Annotation; import java.util.List; import java.util.Set; +import java.util.stream.Stream; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; @@ -15,6 +16,9 @@ import javax.lang.model.type.TypeMirror; import org.javahelpers.simple.builders.processor.util.JavaLangAnalyser; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; /** Unit tests for {@link JavaLangAnalyser}. */ class JavaLangAnalyserTest { @@ -110,21 +114,30 @@ public A[] getAnnotationsByType(Class annotationType) private final VariableElement parameter = new TestVariableElement("testParam"); - @Test - void extractParamJavaDoc_shouldReturnNull_whenJavaDocIsNull() { - assertNull(JavaLangAnalyser.extractParamJavaDoc(null, parameter)); - } - - @Test - void extractParamJavaDoc_shouldReturnNull_whenParameterIsNull() { - assertNull(JavaLangAnalyser.extractParamJavaDoc("Some Javadoc", null)); - } - - @Test - void extractParamJavaDoc_shouldReturnNull_whenParamTagNotFound() { - String javaDoc = - "/**\n * Some method description.\n * @param otherParam some other parameter\n * @return something\n */"; - assertNull(JavaLangAnalyser.extractParamJavaDoc(javaDoc, parameter)); + private static Stream extractParamJavaDocNullCases() { + VariableElement param = new TestVariableElement("testParam"); + return Stream.of( + Arguments.of("JavaDoc is null", null, param), + Arguments.of("Parameter is null", "Some Javadoc", null), + Arguments.of( + "Param tag not found", + "/**\n * Some method description.\n * @param otherParam some other parameter\n * @return something\n */", + param), + Arguments.of( + "Empty param doc", + "/**\n * Some method description.\n * @param testParam\n * @return something\n */", + param), + Arguments.of( + "Whitespace only param doc", + "/**\n * Some method description.\n * @param testParam \n * @return something\n */", + param)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("extractParamJavaDocNullCases") + void extractParamJavaDoc_shouldReturnNull( + String testCase, String javaDoc, VariableElement param) { + assertNull(JavaLangAnalyser.extractParamJavaDoc(javaDoc, param)); } @Test @@ -152,20 +165,6 @@ void extractParamJavaDoc_shouldStopAtNextTag() { "this is a test parameter", JavaLangAnalyser.extractParamJavaDoc(javaDoc, parameter)); } - @Test - void extractParamJavaDoc_shouldHandleEmptyParamDoc() { - String javaDoc = - "/**\n * Some method description.\n * @param testParam\n * @return something\n */"; - assertNull(JavaLangAnalyser.extractParamJavaDoc(javaDoc, parameter)); - } - - @Test - void extractParamJavaDoc_shouldHandleWhitespaceOnlyParamDoc() { - String javaDoc = - "/**\n * Some method description.\n * @param testParam \n * @return something\n */"; - assertNull(JavaLangAnalyser.extractParamJavaDoc(javaDoc, parameter)); - } - @Test void extractParamJavaDoc_shouldHandleWindowsLineEndings() { String javaDoc = @@ -206,16 +205,16 @@ void extractParamJavaDoc_shouldAddSpaceBetweenLines() { @Test void findGetterForField_shouldReturnEmpty_whenDtoTypeIsNull() { - assertTrue(JavaLangAnalyser.findGetterForField(null, "fieldName", null, null, null).isEmpty()); + assertTrue(JavaLangAnalyser.findGetterForField(null, "fieldName", null, null).isEmpty()); } @Test void findGetterForField_shouldReturnEmpty_whenFieldNameIsNull() { - assertTrue(JavaLangAnalyser.findGetterForField(null, null, null, null, null).isEmpty()); + assertTrue(JavaLangAnalyser.findGetterForField(null, null, null, null).isEmpty()); } @Test void findGetterForField_shouldReturnEmpty_whenFieldTypeMirrorIsNull() { - assertTrue(JavaLangAnalyser.findGetterForField(null, "fieldName", null, null, null).isEmpty()); + assertTrue(JavaLangAnalyser.findGetterForField(null, "fieldName", null, null).isEmpty()); } } diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..a243bda1 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,23 @@ +# SonarCloud Configuration +sonar.projectKey=java-helpers_simple-builders +sonar.organization=java-helpers + +# Project Information +sonar.projectName=Simple Builders +sonar.projectVersion=0.2.0 + +# Source and Test Directories +sonar.sources=core/src/main/java,processor/src/main/java +sonar.tests=processor/src/test/java +sonar.java.binaries=core/target/classes,processor/target/classes +sonar.java.test.binaries=processor/target/test-classes + +# Coverage +sonar.coverage.jacoco.xmlReportPaths=processor/target/site/jacoco/jacoco.xml + +# Exclusions +sonar.exclusions=**/generated/**,example/** +sonar.coverage.exclusions=**/*Mapper*.java,**/processor/dtos/**,**/generated/**,example/** + +# Java Version +sonar.java.source=17