v2 extended analysis render

This commit is contained in:
2026-06-14 21:15:04 +02:00
parent 744439c99d
commit 2b6b727de6
18 changed files with 1540 additions and 66 deletions

View File

@@ -5,6 +5,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
import java.util.Map;
@Data
@@ -12,11 +13,23 @@ import java.util.Map;
@Jacksonized
@JsonIgnoreProperties(ignoreUnknown = true)
public class EntryPoint {
public enum Type { REST, KAFKA, RABBIT, JMS, CUSTOM }
public enum Type { REST, KAFKA, RABBIT, JMS, SQS, SNS, CUSTOM }
@Data
@Builder
@Jacksonized
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Parameter {
private final String name;
private final String type;
private final List<String> annotations;
}
private final Type type;
private final String name; // e.g., "POST /orders" or "Kafka Topic: orders"
private final String className;
private final String methodName;
private final Map<String, String> metadata; // e.g., path, topic, exchange
@Builder.Default
private final List<Parameter> parameters = java.util.Collections.emptyList();
}

View File

@@ -4,13 +4,12 @@ import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.CompilationUnit;
import java.nio.file.Path;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -21,6 +20,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
private final ConstantResolver constantResolver = new ConstantResolver();
private final GenericEventDetector eventDetector;
private final SpringMvcDetector mvcDetector;
private final MessagingDetector messagingDetector;
private final CallGraphBuilder callGraphBuilder;
private final LifecycleDetector lifecycleDetector;
private final InterceptorDetector interceptorDetector;
@@ -30,6 +30,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
this.rootDir = rootDir;
this.eventDetector = new GenericEventDetector(context, constantResolver, context.getLibraryHints());
this.mvcDetector = new SpringMvcDetector(context, constantResolver);
this.messagingDetector = new MessagingDetector(context);
this.callGraphBuilder = new CallGraphBuilder(context);
this.lifecycleDetector = new LifecycleDetector(context);
this.interceptorDetector = new InterceptorDetector(context);
@@ -37,7 +38,7 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
@Override
public List<TriggerPoint> findTriggerPoints() {
List<TriggerPoint> allTriggers = new java.util.ArrayList<>();
List<TriggerPoint> allTriggers = new ArrayList<>();
var cus = context.getCompilationUnits();
log.info("JdtIntelligenceProvider scanning {} compilation units for triggers", cus.size());
for (CompilationUnit cu : cus) {
@@ -50,14 +51,15 @@ public class JdtIntelligenceProvider implements CodebaseIntelligenceProvider {
@Override
public List<EntryPoint> findEntryPoints() {
List<EntryPoint> allEntryPoints = new java.util.ArrayList<>();
List<EntryPoint> allEntryPoints = new ArrayList<>();
var cus = context.getCompilationUnits();
log.info("JdtIntelligenceProvider scanning {} compilation units for entry points", cus.size());
for (CompilationUnit cu : cus) {
allEntryPoints.addAll(mvcDetector.detect(cu));
allEntryPoints.addAll(messagingDetector.detect(cu));
allEntryPoints.addAll(interceptorDetector.detect(cu));
}
log.info("Found {} entry points (including interceptors) in total", allEntryPoints.size());
log.info("Found {} entry points (including interceptors and listeners) in total", allEntryPoints.size());
return allEntryPoints;
}

View File

@@ -0,0 +1,132 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.RequiredArgsConstructor;
import org.eclipse.jdt.core.dom.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RequiredArgsConstructor
public class MessagingDetector {
private final CodebaseContext context;
private final ConstantResolver constantResolver = new ConstantResolver();
public List<EntryPoint> detect(CompilationUnit cu) {
List<EntryPoint> entryPoints = new ArrayList<>();
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MethodDeclaration node) {
processMethod(node, entryPoints);
return super.visit(node);
}
});
return entryPoints;
}
private void processMethod(MethodDeclaration method, List<EntryPoint> entryPoints) {
for (Object modifier : method.modifiers()) {
if (modifier instanceof Annotation annotation) {
String typeName = annotation.getTypeName().getFullyQualifiedName();
EntryPoint.Type type = null;
String destination = "";
if (typeName.endsWith("JmsListener")) {
type = EntryPoint.Type.JMS;
destination = resolveValue(annotation, "destination");
} else if (typeName.endsWith("RabbitListener")) {
type = EntryPoint.Type.RABBIT;
destination = resolveValue(annotation, "queues");
} else if (typeName.endsWith("SqsListener")) {
type = EntryPoint.Type.SQS;
destination = resolveValue(annotation, "value");
if ("unknown".equals(destination)) {
destination = resolveValue(annotation, "queueNames");
}
} else if (typeName.endsWith("SnsListener")) {
type = EntryPoint.Type.SNS;
destination = resolveValue(annotation, "value");
if ("unknown".equals(destination)) {
destination = resolveValue(annotation, "topicNames");
}
} else if (typeName.endsWith("KafkaListener")) {
type = EntryPoint.Type.KAFKA;
destination = resolveValue(annotation, "topics");
}
if (type != null) {
Map<String, String> metadata = new HashMap<>();
metadata.put("destination", destination);
String protocolName = type.name();
if (typeName.endsWith("SqsListener")) protocolName = "SQS";
if (typeName.endsWith("SnsListener")) protocolName = "SNS";
metadata.put("protocol", protocolName);
entryPoints.add(EntryPoint.builder()
.type(type)
.name(protocolName + ": " + destination)
.className(context.getFqn((TypeDeclaration) method.getParent()))
.methodName(method.getName().getIdentifier())
.metadata(metadata)
.parameters(extractParameters(method))
.build());
}
}
}
}
private String resolveValue(Annotation annotation, String memberName) {
Expression value = null;
if (annotation instanceof SingleMemberAnnotation sma) {
value = sma.getValue();
} else if (annotation instanceof NormalAnnotation na) {
for (Object pairObj : na.values()) {
MemberValuePair pair = (MemberValuePair) pairObj;
if (memberName.equals(pair.getName().getIdentifier())) {
value = pair.getValue();
break;
}
}
}
if (value != null) {
if (value instanceof ArrayInitializer ai) {
List<String> values = new ArrayList<>();
for (Object o : ai.expressions()) {
String resolved = constantResolver.resolve((Expression) o, context);
values.add(resolved != null ? resolved : o.toString());
}
return String.join(", ", values);
}
String resolved = constantResolver.resolve(value, context);
return resolved != null ? resolved : value.toString();
}
return "unknown";
}
private List<EntryPoint.Parameter> extractParameters(MethodDeclaration method) {
List<EntryPoint.Parameter> parameters = new ArrayList<>();
for (Object paramObj : method.parameters()) {
if (paramObj instanceof SingleVariableDeclaration param) {
List<String> annotations = new ArrayList<>();
for (Object mod : param.modifiers()) {
if (mod instanceof Annotation ann) {
annotations.add(ann.getTypeName().getFullyQualifiedName());
}
}
parameters.add(EntryPoint.Parameter.builder()
.name(param.getName().getIdentifier())
.type(param.getType().toString())
.annotations(annotations)
.build());
}
}
return parameters;
}
}

View File

@@ -36,7 +36,7 @@ public class SpringMvcDetector {
for (Object modifier : node.modifiers()) {
if (modifier instanceof Annotation annotation) {
String name = annotation.getTypeName().getFullyQualifiedName();
if (name.endsWith("RestController") || name.endsWith("Controller")) {
if (name.endsWith("RestController") || name.endsWith("Controller") || name.endsWith("Path")) {
return true;
}
}
@@ -46,6 +46,10 @@ public class SpringMvcDetector {
private void processController(TypeDeclaration controller, List<EntryPoint> entryPoints) {
String basePath = getMappingPathHierarchical(controller);
if (basePath.isEmpty()) {
basePath = getJaxRsPath(controller);
}
for (MethodDeclaration method : controller.getMethods()) {
EntryPoint ep = processMethodHierarchical(method, basePath, controller);
if (ep != null) {
@@ -55,11 +59,9 @@ public class SpringMvcDetector {
}
private String getMappingPathHierarchical(TypeDeclaration td) {
// 1. Check class itself
String path = getMappingPath(td);
if (!path.isEmpty()) return path;
// 2. Check interfaces
for (Object interfaceType : td.superInterfaceTypes()) {
TypeDeclaration itd = context.getTypeDeclaration(interfaceType.toString());
if (itd != null) {
@@ -68,7 +70,6 @@ public class SpringMvcDetector {
}
}
// 3. Check superclass
if (td.getSuperclassType() != null) {
TypeDeclaration std = context.getTypeDeclaration(td.getSuperclassType().toString());
if (std != null) {
@@ -80,11 +81,9 @@ public class SpringMvcDetector {
}
private EntryPoint processMethodHierarchical(MethodDeclaration method, String basePath, TypeDeclaration controller) {
// 1. Check the method itself
EntryPoint ep = processMethod(method, basePath, controller);
if (ep != null) return ep;
// 2. Check interface methods
String methodName = method.getName().getIdentifier();
for (Object interfaceType : controller.superInterfaceTypes()) {
TypeDeclaration itd = context.getTypeDeclaration(interfaceType.toString());
@@ -108,18 +107,24 @@ public class SpringMvcDetector {
String verb = getHttpVerb(name);
if (verb != null) {
String methodPath = getMappingPath(annotation);
if (methodPath.isEmpty()) {
methodPath = getJaxRsPath(method);
}
String fullPath = combinePaths(basePath, methodPath);
Map<String, String> metadata = new HashMap<>();
metadata.put("path", fullPath);
metadata.put("verb", verb);
List<EntryPoint.Parameter> parameters = extractParameters(method);
return EntryPoint.builder()
.type(EntryPoint.Type.REST)
.name(verb + " " + fullPath)
.className(context.getFqn(controller))
.methodName(method.getName().getIdentifier())
.metadata(metadata)
.parameters(parameters)
.build();
}
}
@@ -127,12 +132,21 @@ public class SpringMvcDetector {
return null;
}
private String getJaxRsPath(BodyDeclaration node) {
for (Object mod : node.modifiers()) {
if (mod instanceof Annotation ann && ann.getTypeName().getFullyQualifiedName().endsWith("Path")) {
return getMappingPath(ann);
}
}
return "";
}
private String getHttpVerb(String annotationName) {
if (annotationName.endsWith("PostMapping")) return "POST";
if (annotationName.endsWith("GetMapping")) return "GET";
if (annotationName.endsWith("PutMapping")) return "PUT";
if (annotationName.endsWith("DeleteMapping")) return "DELETE";
if (annotationName.endsWith("PatchMapping")) return "PATCH";
if (annotationName.endsWith("PostMapping") || annotationName.endsWith("POST")) return "POST";
if (annotationName.endsWith("GetMapping") || annotationName.endsWith("GET")) return "GET";
if (annotationName.endsWith("PutMapping") || annotationName.endsWith("PUT")) return "PUT";
if (annotationName.endsWith("DeleteMapping") || annotationName.endsWith("DELETE")) return "DELETE";
if (annotationName.endsWith("PatchMapping") || annotationName.endsWith("PATCH")) return "PATCH";
if (annotationName.endsWith("RequestMapping")) return "REQUEST";
return null;
}
@@ -140,7 +154,8 @@ public class SpringMvcDetector {
private String getMappingPath(TypeDeclaration td) {
for (Object modifier : td.modifiers()) {
if (modifier instanceof Annotation annotation) {
if (annotation.getTypeName().getFullyQualifiedName().endsWith("RequestMapping")) {
String name = annotation.getTypeName().getFullyQualifiedName();
if (name.endsWith("RequestMapping") || name.endsWith("Path")) {
return getMappingPath(annotation);
}
}
@@ -169,6 +184,29 @@ public class SpringMvcDetector {
return "";
}
private List<EntryPoint.Parameter> extractParameters(MethodDeclaration method) {
List<EntryPoint.Parameter> parameters = new ArrayList<>();
for (Object paramObj : method.parameters()) {
if (paramObj instanceof SingleVariableDeclaration param) {
List<String> annotations = new ArrayList<>();
for (Object mod : param.modifiers()) {
if (mod instanceof Annotation ann) {
String name = ann.getTypeName().getFullyQualifiedName();
if (name.endsWith("PathParam")) name = "PathVariable";
if (name.endsWith("QueryParam")) name = "RequestParam";
annotations.add(name);
}
}
parameters.add(EntryPoint.Parameter.builder()
.name(param.getName().getIdentifier())
.type(param.getType().toString())
.annotations(annotations)
.build());
}
}
return parameters;
}
private String combinePaths(String base, String method) {
String b = base.startsWith("/") ? base : "/" + base;
if (b.endsWith("/")) b = b.substring(0, b.length() - 1);