orphan removed
All checks were successful
Release / release (push) Successful in 37s

This commit is contained in:
2026-03-09 22:24:10 +01:00
parent 344d24dec9
commit 2dd3a687a5

View File

@@ -1,396 +0,0 @@
package org.egothor.methodatlas;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.ParserConfiguration.LanguageLevel;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.ArrayInitializerExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MemberValuePair;
import org.egothor.methodatlas.ai.AiClassSuggestion;
import org.egothor.methodatlas.ai.AiMethodSuggestion;
import org.egothor.methodatlas.ai.AiOptions;
import org.egothor.methodatlas.ai.AiProvider;
import org.egothor.methodatlas.ai.AiSuggestionEngine;
import org.egothor.methodatlas.ai.AiSuggestionEngineImpl;
import org.egothor.methodatlas.ai.AiSuggestionException;
import org.egothor.methodatlas.ai.SuggestionLookup;
public class MethodAtlasApp {
private static final Logger LOG = Logger.getLogger(MethodAtlasApp.class.getName());
private enum OutputMode {
CSV,
PLAIN
}
private record CliConfig(
OutputMode outputMode,
AiOptions aiOptions,
List<Path> paths
) {
}
public static void main(String[] args) throws IOException {
ParserConfiguration parserConfiguration = new ParserConfiguration();
parserConfiguration.setLanguageLevel(LanguageLevel.JAVA_21);
StaticJavaParser.setConfiguration(parserConfiguration);
CliConfig cliConfig = parseArgs(args);
AiSuggestionEngine aiEngine = buildAiEngine(cliConfig.aiOptions());
if (cliConfig.outputMode() == OutputMode.CSV) {
if (cliConfig.aiOptions().enabled()) {
System.out.println("fqcn,method,loc,tags,ai_security_relevant,ai_display_name,ai_tags,ai_reason");
} else {
System.out.println("fqcn,method,loc,tags");
}
}
if (cliConfig.paths().isEmpty()) {
scanRoot(Paths.get("."), cliConfig.outputMode(), cliConfig.aiOptions(), aiEngine);
return;
}
for (Path path : cliConfig.paths()) {
scanRoot(path, cliConfig.outputMode(), cliConfig.aiOptions(), aiEngine);
}
}
private static void scanRoot(
Path root,
OutputMode mode,
AiOptions aiOptions,
AiSuggestionEngine aiEngine
) throws IOException {
LOG.log(Level.INFO, "Scanning {0} for JUnit files", root);
try (Stream<Path> stream = Files.walk(root)) {
stream.filter(path -> path.toString().endsWith("Test.java"))
.forEach(path -> processFile(path, mode, aiOptions, aiEngine));
}
}
private static void processFile(
Path path,
OutputMode mode,
AiOptions aiOptions,
AiSuggestionEngine aiEngine
) {
try {
CompilationUnit compilationUnit = StaticJavaParser.parse(path);
String packageName = compilationUnit.getPackageDeclaration()
.map(packageDeclaration -> packageDeclaration.getNameAsString())
.orElse("");
compilationUnit.findAll(ClassOrInterfaceDeclaration.class).forEach(clazz -> {
String className = clazz.getNameAsString();
String fqcn = packageName.isEmpty() ? className : packageName + "." + className;
SuggestionLookup suggestionLookup = resolveSuggestionLookup(clazz, fqcn, aiOptions, aiEngine);
clazz.findAll(MethodDeclaration.class).forEach(method -> {
if (!isJUnitTest(method)) {
return;
}
int loc = countLOC(method);
List<String> tags = getTagValues(method);
AiMethodSuggestion suggestion = suggestionLookup.find(method.getNameAsString()).orElse(null);
emit(
mode,
aiOptions.enabled(),
fqcn,
method.getNameAsString(),
loc,
tags,
suggestion
);
});
});
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to parse: {0}", path);
e.printStackTrace();
}
}
private static SuggestionLookup resolveSuggestionLookup(
ClassOrInterfaceDeclaration clazz,
String fqcn,
AiOptions aiOptions,
AiSuggestionEngine aiEngine
) {
if (!aiOptions.enabled() || aiEngine == null) {
return SuggestionLookup.from(null);
}
String classSource = clazz.toString();
if (classSource.length() > aiOptions.maxClassChars()) {
LOG.log(
Level.INFO,
"Skipping AI for {0}: class source too large ({1} chars)",
new Object[] { fqcn, classSource.length() }
);
return SuggestionLookup.from(null);
}
try {
AiClassSuggestion aiClassSuggestion = aiEngine.suggestForClass(fqcn, classSource);
return SuggestionLookup.from(aiClassSuggestion);
} catch (AiSuggestionException e) {
LOG.log(Level.WARNING, "AI suggestion failed for class " + fqcn, e);
return SuggestionLookup.from(null);
}
}
private static AiSuggestionEngine buildAiEngine(AiOptions aiOptions) {
if (!aiOptions.enabled()) {
return null;
}
try {
return new AiSuggestionEngineImpl(aiOptions);
} catch (AiSuggestionException e) {
throw new IllegalStateException("Failed to initialize AI engine", e);
}
}
private static void emit(
OutputMode mode,
boolean aiEnabled,
String fqcn,
String method,
int loc,
List<String> tags,
AiMethodSuggestion suggestion
) {
if (mode == OutputMode.PLAIN) {
emitPlain(aiEnabled, fqcn, method, loc, tags, suggestion);
return;
}
emitCsv(aiEnabled, fqcn, method, loc, tags, suggestion);
}
private static void emitPlain(
boolean aiEnabled,
String fqcn,
String method,
int loc,
List<String> tags,
AiMethodSuggestion suggestion
) {
String existingTags = tags.isEmpty() ? "-" : String.join(";", tags);
if (!aiEnabled) {
System.out.println(fqcn + ", " + method + ", LOC=" + loc + ", TAGS=" + existingTags);
return;
}
String aiSecurity = suggestion == null ? "-" : Boolean.toString(suggestion.securityRelevant());
String aiDisplayName = suggestion == null || suggestion.displayName() == null ? "-" : suggestion.displayName();
String aiTags = suggestion == null || suggestion.tags() == null || suggestion.tags().isEmpty()
? "-"
: String.join(";", suggestion.tags());
String aiReason = suggestion == null || suggestion.reason() == null || suggestion.reason().isBlank()
? "-"
: suggestion.reason();
System.out.println(
fqcn + ", " + method
+ ", LOC=" + loc
+ ", TAGS=" + existingTags
+ ", AI_SECURITY=" + aiSecurity
+ ", AI_DISPLAY=" + aiDisplayName
+ ", AI_TAGS=" + aiTags
+ ", AI_REASON=" + aiReason
);
}
private static void emitCsv(
boolean aiEnabled,
String fqcn,
String method,
int loc,
List<String> tags,
AiMethodSuggestion suggestion
) {
String existingTags = tags.isEmpty() ? "" : String.join(";", tags);
if (!aiEnabled) {
System.out.println(
csvEscape(fqcn) + ","
+ csvEscape(method) + ","
+ loc + ","
+ csvEscape(existingTags)
);
return;
}
String aiSecurity = suggestion == null ? "" : Boolean.toString(suggestion.securityRelevant());
String aiDisplayName = suggestion == null || suggestion.displayName() == null ? "" : suggestion.displayName();
String aiTags = suggestion == null || suggestion.tags() == null ? "" : String.join(";", suggestion.tags());
String aiReason = suggestion == null || suggestion.reason() == null ? "" : suggestion.reason();
System.out.println(
csvEscape(fqcn) + ","
+ csvEscape(method) + ","
+ loc + ","
+ csvEscape(existingTags) + ","
+ csvEscape(aiSecurity) + ","
+ csvEscape(aiDisplayName) + ","
+ csvEscape(aiTags) + ","
+ csvEscape(aiReason)
);
}
private static String csvEscape(String value) {
if (value == null) {
return "";
}
boolean mustQuote =
value.indexOf(',') >= 0
|| value.indexOf('"') >= 0
|| value.indexOf('\n') >= 0
|| value.indexOf('\r') >= 0;
if (!mustQuote) {
return value;
}
return "\"" + value.replace("\"", "\"\"") + "\"";
}
private static CliConfig parseArgs(String[] args) {
OutputMode outputMode = OutputMode.CSV;
List<Path> paths = new ArrayList<>();
AiOptions.Builder aiBuilder = AiOptions.builder();
for (int i = 0; i < args.length; i++) {
String arg = args[i];
switch (arg) {
case "-plain" -> outputMode = OutputMode.PLAIN;
case "-ai" -> aiBuilder.enabled(true);
case "-ai-provider" -> aiBuilder.provider(
AiProvider.valueOf(nextArg(args, ++i, arg).toUpperCase())
);
case "-ai-model" -> aiBuilder.modelName(nextArg(args, ++i, arg));
case "-ai-base-url" -> aiBuilder.baseUrl(nextArg(args, ++i, arg));
case "-ai-api-key" -> aiBuilder.apiKey(nextArg(args, ++i, arg));
case "-ai-api-key-env" -> aiBuilder.apiKeyEnv(nextArg(args, ++i, arg));
case "-ai-taxonomy" -> aiBuilder.taxonomyFile(Paths.get(nextArg(args, ++i, arg)));
case "-ai-taxonomy-mode" -> aiBuilder.taxonomyMode(
AiOptions.TaxonomyMode.valueOf(nextArg(args, ++i, arg).toUpperCase())
);
case "-ai-max-class-chars" -> aiBuilder.maxClassChars(
Integer.parseInt(nextArg(args, ++i, arg))
);
case "-ai-timeout-sec" -> aiBuilder.timeout(
Duration.ofSeconds(Long.parseLong(nextArg(args, ++i, arg)))
);
case "-ai-max-retries" -> aiBuilder.maxRetries(
Integer.parseInt(nextArg(args, ++i, arg))
);
default -> {
if (arg.startsWith("-")) {
throw new IllegalArgumentException("Unknown argument: " + arg);
}
paths.add(Paths.get(arg));
}
}
}
return new CliConfig(outputMode, aiBuilder.build(), paths);
}
private static String nextArg(String[] args, int index, String option) {
if (index >= args.length) {
throw new IllegalArgumentException("Missing value for " + option);
}
return args[index];
}
private static boolean isJUnitTest(MethodDeclaration method) {
for (AnnotationExpr annotation : method.getAnnotations()) {
String name = annotation.getNameAsString();
if ("Test".equals(name) || "ParameterizedTest".equals(name) || "RepeatedTest".equals(name)) {
return true;
}
}
return false;
}
private static int countLOC(MethodDeclaration method) {
return method.getRange()
.map(range -> range.end.line - range.begin.line + 1)
.orElse(0);
}
private static List<String> getTagValues(MethodDeclaration method) {
List<String> tagValues = new ArrayList<>();
for (AnnotationExpr annotation : method.getAnnotations()) {
String name = annotation.getNameAsString();
if ("Tag".equals(name)) {
extractTagValue(annotation).ifPresent(tagValues::add);
} else if ("Tags".equals(name) && annotation.isNormalAnnotationExpr()) {
for (MemberValuePair pair : annotation.asNormalAnnotationExpr().getPairs()) {
if ("value".equals(pair.getNameAsString()) && pair.getValue().isArrayInitializerExpr()) {
ArrayInitializerExpr array = pair.getValue().asArrayInitializerExpr();
for (Expression expression : array.getValues()) {
if (expression.isAnnotationExpr()) {
extractTagValue(expression.asAnnotationExpr()).ifPresent(tagValues::add);
}
}
}
}
}
}
return tagValues;
}
private static Optional<String> extractTagValue(AnnotationExpr annotation) {
if (!"Tag".equals(annotation.getNameAsString())) {
return Optional.empty();
}
if (annotation.isSingleMemberAnnotationExpr()) {
Expression memberValue = annotation.asSingleMemberAnnotationExpr().getMemberValue();
if (memberValue.isStringLiteralExpr()) {
return Optional.of(memberValue.asStringLiteralExpr().asString());
}
}
if (annotation.isNormalAnnotationExpr()) {
for (MemberValuePair pair : annotation.asNormalAnnotationExpr().getPairs()) {
if ("value".equals(pair.getNameAsString()) && pair.getValue().isStringLiteralExpr()) {
return Optional.of(pair.getValue().asStringLiteralExpr().asString());
}
}
}
return Optional.empty();
}
}