TagApplier.java
package org.egothor.methodatlas;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.egothor.methodatlas.ai.AiMethodSuggestion;
import org.egothor.methodatlas.ai.SuggestionLookup;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.StringLiteralExpr;
/**
* Applies AI-generated {@code @DisplayName} and {@code @Tag} annotations to
* security-relevant JUnit test methods in a parsed class declaration.
*
* <p>
* For each test method in the class whose AI suggestion marks it as
* security-relevant, this class:
* </p>
* <ul>
* <li>inserts {@code @DisplayName("<text>")} if the suggestion provides a
* non-blank display name and the annotation is not already present</li>
* <li>inserts {@code @Tag("<tag>")} for each security tag in the suggestion
* that is not already declared on the method</li>
* </ul>
*
* <p>
* Caller is responsible for managing JUnit imports on the enclosing
* {@link com.github.javaparser.ast.CompilationUnit} based on the
* {@link ClassResult#displayNamesAdded()} and {@link ClassResult#tagsAdded()}
* counts. The constants {@link #IMPORT_DISPLAY_NAME} and {@link #IMPORT_TAG}
* provide the fully qualified import strings.
* </p>
*
* <p>
* Only direct methods of the supplied class declaration are processed; methods
* belonging to inner classes are handled when those inner classes are processed
* as separate entries in the caller's iteration.
* </p>
*
* <p>
* This class is a non-instantiable utility holder.
* </p>
*
* @see MethodAtlasApp
* @see AnnotationInspector
*/
final class TagApplier {
/** Fully qualified name of {@code @DisplayName} for import management. */
/* default */ static final String IMPORT_DISPLAY_NAME = "org.junit.jupiter.api.DisplayName";
/** Fully qualified name of {@code @Tag} for import management. */
/* default */ static final String IMPORT_TAG = "org.junit.jupiter.api.Tag";
private static final String ANNOTATION_DISPLAY_NAME = "DisplayName";
private static final String ANNOTATION_TAG = "Tag";
/**
* Prevents instantiation of this utility class.
*/
private TagApplier() {
}
/**
* Result of applying annotations to a single class declaration.
*
* @param annotationsAdded total count of annotations inserted
* @param displayNamesAdded number of {@code @DisplayName} annotations inserted
* @param tagsAdded number of {@code @Tag} annotations inserted
*/
/* default */ record ClassResult(int annotationsAdded, int displayNamesAdded, int tagsAdded) {
/**
* Returns {@code true} when at least one annotation was inserted.
*
* @return {@code true} if the class was modified
*/
/* default */ boolean modified() {
return annotationsAdded > 0;
}
}
/**
* Applies security annotations to the direct test methods of {@code clazz}.
*
* <p>
* Only methods that are directly declared in {@code clazz} (not in nested
* inner classes) are considered. Methods in inner classes are expected to be
* processed when those inner classes are encountered in the caller's
* iteration.
* </p>
*
* <p>
* No imports are modified by this method; the caller should inspect
* {@link ClassResult#displayNamesAdded()} and {@link ClassResult#tagsAdded()}
* and add the corresponding imports to the enclosing compilation unit when
* the counts are non-zero.
* </p>
*
* @param clazz class declaration whose methods should be annotated
* @param lookup AI suggestions indexed by method name
* @param testAnnotations annotation simple names identifying test methods
* @return result describing what was changed; never {@code null}
*/
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
/* default */ static ClassResult applyToClass(ClassOrInterfaceDeclaration clazz, SuggestionLookup lookup,
Set<String> testAnnotations) {
int displayNamesAdded = 0;
int tagsAdded = 0;
for (MethodDeclaration method : clazz.getMethods()) {
if (!AnnotationInspector.isJUnitTest(method, testAnnotations)) {
continue;
}
AiMethodSuggestion suggestion = lookup.find(method.getNameAsString()).orElse(null);
if (suggestion == null || !suggestion.securityRelevant()) {
continue;
}
// Add @DisplayName if the suggestion provides one and it is not yet present.
if (suggestion.displayName() != null && !suggestion.displayName().isBlank()
&& !hasAnnotation(method, ANNOTATION_DISPLAY_NAME)) {
method.addSingleMemberAnnotation(ANNOTATION_DISPLAY_NAME,
new StringLiteralExpr(suggestion.displayName()));
displayNamesAdded++;
}
// Add @Tag for each security tag not already on the method.
Set<String> existingTags = new HashSet<>(AnnotationInspector.getTagValues(method));
List<String> suggestionTags = suggestion.tags();
if (suggestionTags != null) {
for (String tag : suggestionTags) {
if (tag != null && !tag.isBlank() && existingTags.add(tag)) {
method.addSingleMemberAnnotation(ANNOTATION_TAG, new StringLiteralExpr(tag));
tagsAdded++;
}
}
}
}
return new ClassResult(displayNamesAdded + tagsAdded, displayNamesAdded, tagsAdded);
}
/**
* Returns {@code true} if the method already carries an annotation with the
* given simple name.
*
* @param method method declaration to inspect
* @param simpleName annotation simple name to look for (e.g.
* {@code "DisplayName"})
* @return {@code true} if a matching annotation is present
*/
private static boolean hasAnnotation(MethodDeclaration method, String simpleName) {
return method.getAnnotations().stream()
.anyMatch(a -> simpleName.equals(a.getNameAsString()));
}
}