inhertiance
This commit is contained in:
@@ -4,19 +4,32 @@ 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.ast.common.CodebaseContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class CallGraphBuilder {
|
||||
private final CodebaseContext context;
|
||||
private final ConstantResolver constantResolver;
|
||||
private String currentMethodFqn;
|
||||
private Map<String, List<CallEdge>> graph;
|
||||
|
||||
@lombok.Data
|
||||
private static class CallEdge {
|
||||
private final String targetMethod;
|
||||
private final List<String> arguments;
|
||||
}
|
||||
|
||||
public CallGraphBuilder(CodebaseContext context) {
|
||||
this.context = context;
|
||||
this.constantResolver = new ConstantResolver();
|
||||
}
|
||||
|
||||
public List<CallChain> findChains(List<EntryPoint> entryPoints, List<TriggerPoint> triggers) {
|
||||
Map<String, List<String>> callGraph = buildCallGraph();
|
||||
Map<String, List<CallEdge>> callGraph = buildCallGraph();
|
||||
List<CallChain> chains = new ArrayList<>();
|
||||
|
||||
for (EntryPoint ep : entryPoints) {
|
||||
@@ -25,9 +38,10 @@ public class CallGraphBuilder {
|
||||
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
||||
List<String> path = findPath(startMethod, targetMethod, callGraph, new HashSet<>());
|
||||
if (path != null) {
|
||||
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph);
|
||||
chains.add(CallChain.builder()
|
||||
.entryPoint(ep)
|
||||
.triggerPoint(tp)
|
||||
.triggerPoint(resolvedTp)
|
||||
.methodChain(path)
|
||||
.build());
|
||||
}
|
||||
@@ -36,12 +50,57 @@ public class CallGraphBuilder {
|
||||
return chains;
|
||||
}
|
||||
|
||||
private Map<String, List<String>> buildCallGraph() {
|
||||
Map<String, List<String>> graph = new HashMap<>();
|
||||
private TriggerPoint resolveTriggerPointParameters(TriggerPoint tp, List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||
if (path.size() < 2) return tp;
|
||||
|
||||
String event = tp.getEvent();
|
||||
if (event == null || event.isEmpty() || event.contains("\"")) return tp;
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration(tp.getClassName());
|
||||
if (td != null) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, tp.getMethodName(), true);
|
||||
if (md != null) {
|
||||
int paramIndex = -1;
|
||||
for (int i = 0; i < md.parameters().size(); i++) {
|
||||
SingleVariableDeclaration svd = (SingleVariableDeclaration) md.parameters().get(i);
|
||||
if (svd.getName().getIdentifier().equals(event)) {
|
||||
paramIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (paramIndex >= 0) {
|
||||
String caller = path.get(path.size() - 2);
|
||||
String target = path.get(path.size() - 1);
|
||||
List<CallEdge> edges = callGraph.get(caller);
|
||||
if (edges != null) {
|
||||
for (CallEdge edge : edges) {
|
||||
if (edge.getTargetMethod().equals(target) || isHeuristicMatch(edge.getTargetMethod(), target)) {
|
||||
if (paramIndex < edge.getArguments().size()) {
|
||||
String resolvedValue = edge.getArguments().get(paramIndex);
|
||||
if (resolvedValue != null) {
|
||||
return TriggerPoint.builder()
|
||||
.event(resolvedValue)
|
||||
.className(tp.getClassName())
|
||||
.methodName(tp.getMethodName())
|
||||
.sourceFile(tp.getSourceFile())
|
||||
.lineNumber(tp.getLineNumber())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tp;
|
||||
}
|
||||
|
||||
private Map<String, List<CallEdge>> buildCallGraph() {
|
||||
graph = new HashMap<>();
|
||||
for (CompilationUnit cu : context.getCompilationUnits()) {
|
||||
cu.accept(new ASTVisitor() {
|
||||
private String currentMethodFqn;
|
||||
|
||||
@Override
|
||||
public boolean visit(MethodDeclaration node) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
@@ -55,8 +114,31 @@ public class CallGraphBuilder {
|
||||
public boolean visit(MethodInvocation node) {
|
||||
if (currentMethodFqn != null) {
|
||||
List<String> calledMethods = resolveCalledMethodsPolymorphic(node);
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
for (String calledMethod : calledMethods) {
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(calledMethod);
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(SuperMethodInvocation node) {
|
||||
if (currentMethodFqn != null) {
|
||||
String methodName = node.getName().getIdentifier();
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
String superFqn = context.getSuperclassFqn(td);
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
if (superTd != null) {
|
||||
String calledMethod = resolveMethodInType(superTd, methodName);
|
||||
if (calledMethod != null) {
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
graph.computeIfAbsent(currentMethodFqn, k -> new ArrayList<>()).add(new CallEdge(calledMethod, args));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.visit(node);
|
||||
@@ -66,6 +148,19 @@ public class CallGraphBuilder {
|
||||
return graph;
|
||||
}
|
||||
|
||||
private List<String> resolveArguments(List<?> astArguments) {
|
||||
List<String> args = new ArrayList<>();
|
||||
for (Object argObj : astArguments) {
|
||||
Expression expr = (Expression) argObj;
|
||||
String val = constantResolver.resolve(expr, context);
|
||||
if (val == null && expr instanceof SimpleName sn) {
|
||||
val = sn.getIdentifier();
|
||||
}
|
||||
args.add(val);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
private List<String> resolveCalledMethodsPolymorphic(MethodInvocation node) {
|
||||
String baseCalled = resolveCalledMethod(node);
|
||||
if (baseCalled == null) return Collections.emptyList();
|
||||
@@ -73,7 +168,6 @@ public class CallGraphBuilder {
|
||||
List<String> allResolved = new ArrayList<>();
|
||||
allResolved.add(baseCalled);
|
||||
|
||||
// Polymorphic fan-out
|
||||
if (baseCalled.contains(".")) {
|
||||
String className = baseCalled.substring(0, baseCalled.lastIndexOf('.'));
|
||||
String methodName = baseCalled.substring(baseCalled.lastIndexOf('.') + 1);
|
||||
@@ -92,39 +186,43 @@ public class CallGraphBuilder {
|
||||
String methodName = node.getName().getIdentifier();
|
||||
|
||||
if (receiver == null) {
|
||||
// Local call or static import (simplified)
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
return td != null ? context.getFqn(td) + "." + methodName : null;
|
||||
if (td != null) {
|
||||
return resolveMethodInType(td, methodName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to resolve receiver type
|
||||
// This is a simplified resolution without full JDT bindings.
|
||||
// It works for local variables and fields within the same project.
|
||||
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||
if (binding != null) {
|
||||
return binding.getQualifiedName() + "." + methodName;
|
||||
}
|
||||
|
||||
// Fallback: heuristic resolution for simple cases like 'orderService.process()'
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
String receiverName = sn.getIdentifier();
|
||||
// Look for fields or variables (simplified)
|
||||
// For now, we'll return a "best guess" if we can't resolve it perfectly
|
||||
// In a real implementation, we'd use JDT bindings more heavily.
|
||||
return receiverName + "." + methodName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> findPath(String start, String target, Map<String, List<String>> graph, Set<String> visited) {
|
||||
private String resolveMethodInType(TypeDeclaration td, String methodName) {
|
||||
MethodDeclaration md = context.findMethodDeclaration(td, methodName, true);
|
||||
if (md != null) {
|
||||
TypeDeclaration declaringTd = findEnclosingType(md);
|
||||
return context.getFqn(declaringTd) + "." + methodName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> findPath(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
||||
if (start.equals(target)) return new ArrayList<>(List.of(start));
|
||||
if (!visited.add(start)) return null;
|
||||
|
||||
List<String> neighbors = graph.get(start);
|
||||
List<CallEdge> neighbors = graph.get(start);
|
||||
if (neighbors != null) {
|
||||
for (String neighbor : neighbors) {
|
||||
// Heuristic: neighbor might be 'orderService.process' while target is 'click.kamil...OrderService.process'
|
||||
for (CallEdge edge : neighbors) {
|
||||
String neighbor = edge.getTargetMethod();
|
||||
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
||||
return new ArrayList<>(List.of(start, target));
|
||||
}
|
||||
@@ -139,14 +237,10 @@ public class CallGraphBuilder {
|
||||
}
|
||||
|
||||
private boolean isHeuristicMatch(String neighbor, String target) {
|
||||
// Match 'orderService.process' with 'click.kamil.OrderService.process'
|
||||
if (target.endsWith("." + neighbor)) return true;
|
||||
// Match simple names if we don't have FQN
|
||||
String simpleTarget = target.contains(".") ? target.substring(target.lastIndexOf('.') + 1) : target;
|
||||
String simpleNeighbor = neighbor.contains(".") ? neighbor.substring(neighbor.lastIndexOf('.') + 1) : neighbor;
|
||||
|
||||
// Very basic heuristic for now
|
||||
return neighbor.toLowerCase().contains(simpleTarget.toLowerCase()) || target.toLowerCase().contains(simpleNeighbor.toLowerCase());
|
||||
return simpleNeighbor.equals(simpleTarget);
|
||||
}
|
||||
|
||||
private TypeDeclaration findEnclosingType(ASTNode node) {
|
||||
|
||||
@@ -21,7 +21,11 @@ public class GenericEventDetector {
|
||||
|
||||
public List<TriggerPoint> detect(CompilationUnit cu) {
|
||||
List<TriggerPoint> triggers = new ArrayList<>();
|
||||
String fileName = cu.getJavaElement() != null ? cu.getJavaElement().getElementName() : "unknown";
|
||||
String fileName = "unknown";
|
||||
try {
|
||||
if (cu.getJavaElement() != null) fileName = cu.getJavaElement().getElementName();
|
||||
} catch (Exception e) {}
|
||||
|
||||
log.debug("Scanning for triggers in {}", fileName);
|
||||
|
||||
cu.accept(new ASTVisitor() {
|
||||
@@ -29,12 +33,10 @@ public class GenericEventDetector {
|
||||
public boolean visit(MethodInvocation node) {
|
||||
String methodName = node.getName().getIdentifier();
|
||||
|
||||
// 1. Core sendEvent
|
||||
if ("sendEvent".equals(methodName)) {
|
||||
processSendEvent(node, cu, triggers);
|
||||
}
|
||||
|
||||
// 2. Library Hints
|
||||
processHints(node, cu, triggers);
|
||||
|
||||
return super.visit(node);
|
||||
@@ -44,10 +46,9 @@ public class GenericEventDetector {
|
||||
}
|
||||
|
||||
private void processSendEvent(MethodInvocation node, CompilationUnit cu, List<TriggerPoint> triggers) {
|
||||
log.info("Found potential trigger: {}", node);
|
||||
TriggerPoint trigger = buildTriggerPoint(node, cu, null);
|
||||
if (trigger != null) {
|
||||
log.info("Successfully built trigger point: {}", trigger.getEvent());
|
||||
log.debug("Successfully built trigger point: {}", trigger.getEvent());
|
||||
triggers.add(trigger);
|
||||
}
|
||||
}
|
||||
@@ -57,15 +58,12 @@ public class GenericEventDetector {
|
||||
|
||||
String calledMethod = resolveCalledMethodName(node);
|
||||
if (calledMethod == null) return;
|
||||
|
||||
log.info("Checking method call '{}' against {} hints", calledMethod, hints.size());
|
||||
|
||||
for (LibraryHint hint : hints) {
|
||||
if (calledMethod.equals(hint.getMethodFqn()) || isHintMatch(calledMethod, hint.getMethodFqn())) {
|
||||
log.info("MATCH! Found hint for '{}'", calledMethod);
|
||||
TriggerPoint trigger = buildTriggerPoint(node, cu, hint.getEvent());
|
||||
if (trigger != null) {
|
||||
log.info("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
||||
log.debug("Successfully built synthetic trigger point from hint: {}", trigger.getEvent());
|
||||
triggers.add(trigger);
|
||||
}
|
||||
}
|
||||
@@ -104,15 +102,20 @@ public class GenericEventDetector {
|
||||
}
|
||||
|
||||
private TriggerPoint buildTriggerPoint(MethodInvocation node, CompilationUnit cu, String forcedEvent) {
|
||||
String eventValue;
|
||||
String eventValue = null;
|
||||
if (forcedEvent != null) {
|
||||
eventValue = forcedEvent;
|
||||
eventValue = context.resolveString(forcedEvent);
|
||||
} else {
|
||||
if (node.arguments().isEmpty()) return null;
|
||||
Expression eventExpr = (Expression) node.arguments().get(0);
|
||||
eventValue = constantResolver.resolve(eventExpr, context);
|
||||
if (eventValue == null) {
|
||||
eventValue = eventExpr.toString();
|
||||
|
||||
// Try MessageBuilder extraction first
|
||||
eventValue = extractEventFromMessageBuilder(eventExpr);
|
||||
|
||||
if (eventValue != null) {
|
||||
eventValue = context.resolveString(eventValue);
|
||||
} else {
|
||||
eventValue = context.resolveExpression(eventExpr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +133,35 @@ public class GenericEventDetector {
|
||||
.build();
|
||||
}
|
||||
|
||||
private String extractEventFromMessageBuilder(Expression expr) {
|
||||
if (!(expr instanceof MethodInvocation mi)) return null;
|
||||
|
||||
// Trace back chain: MessageBuilder.withPayload(event).setHeader(...).build()
|
||||
MethodInvocation current = mi;
|
||||
while (current != null) {
|
||||
String name = current.getName().getIdentifier();
|
||||
if ("withPayload".equals(name) && !current.arguments().isEmpty()) {
|
||||
Expression payloadExpr = (Expression) current.arguments().get(0);
|
||||
String resolved = constantResolver.resolve(payloadExpr, context);
|
||||
if (resolved != null) return resolved;
|
||||
|
||||
// If not a constant, it might be a parameter like 'event'
|
||||
if (payloadExpr instanceof SimpleName sn) {
|
||||
return sn.getIdentifier(); // Return the parameter name so the call graph can fill it
|
||||
}
|
||||
return payloadExpr.toString();
|
||||
}
|
||||
|
||||
Expression receiver = current.getExpression();
|
||||
if (receiver instanceof MethodInvocation nextMi) {
|
||||
current = nextMi;
|
||||
} else {
|
||||
current = null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private MethodDeclaration findEnclosingMethod(ASTNode node) {
|
||||
ASTNode parent = node.getParent();
|
||||
while (parent != null && !(parent instanceof MethodDeclaration)) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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.*;
|
||||
@@ -14,7 +13,6 @@ 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<>();
|
||||
@@ -99,13 +97,11 @@ public class MessagingDetector {
|
||||
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());
|
||||
values.add(context.resolveExpression((Expression) o));
|
||||
}
|
||||
return String.join(", ", values);
|
||||
}
|
||||
String resolved = constantResolver.resolve(value, context);
|
||||
return resolved != null ? resolved : value.toString();
|
||||
return context.resolveExpression(value);
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
@@ -44,13 +44,7 @@ public class SpringMvcDetector {
|
||||
String verb = getHttpVerb(name);
|
||||
if (verb != null && !call.arguments().isEmpty()) {
|
||||
Expression pathArg = (Expression) call.arguments().get(0);
|
||||
String path = constantResolver.resolve(pathArg, context);
|
||||
if (path != null) {
|
||||
path = context.resolveString(path);
|
||||
} else {
|
||||
path = pathArg.toString().replaceAll("\"", "");
|
||||
path = context.resolveString(path);
|
||||
}
|
||||
String path = context.resolveExpression(pathArg);
|
||||
|
||||
entryPoints.add(EntryPoint.builder()
|
||||
.type(EntryPoint.Type.REST)
|
||||
@@ -224,11 +218,7 @@ public class SpringMvcDetector {
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
String resolved = constantResolver.resolve(value, context);
|
||||
if (resolved == null) {
|
||||
resolved = value.toString().replaceAll("^\"|\"$", "");
|
||||
}
|
||||
return context.resolveString(resolved);
|
||||
return context.resolveExpression(value);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -114,6 +114,17 @@ public class CodebaseContext {
|
||||
|
||||
return PlaceholderResolver.resolve(input, merged);
|
||||
}
|
||||
|
||||
public String resolveExpression(Expression expr) {
|
||||
if (expr == null) return null;
|
||||
|
||||
String astResolved = constantResolver.resolve(expr, this);
|
||||
if (astResolved == null) {
|
||||
astResolved = expr.toString().replaceAll("^\"|\"$", "");
|
||||
}
|
||||
|
||||
return resolveString(astResolved);
|
||||
}
|
||||
public TypeDeclaration resolveStaticImport(String memberName, CompilationUnit contextCu) {
|
||||
if (contextCu == null) return null;
|
||||
|
||||
@@ -416,6 +427,57 @@ public class CodebaseContext {
|
||||
return getTypeDeclaration(name);
|
||||
}
|
||||
|
||||
public String getSuperclassFqn(TypeDeclaration td) {
|
||||
Type superType = td.getSuperclassType();
|
||||
if (superType == null) return null;
|
||||
|
||||
String superName = extractTypeName(superType);
|
||||
CompilationUnit cu = (CompilationUnit) td.getRoot();
|
||||
TypeDeclaration superTd = getTypeDeclaration(superName, cu);
|
||||
return superTd != null ? getFqn(superTd) : superName;
|
||||
}
|
||||
|
||||
private String extractTypeName(Type type) {
|
||||
if (type.isParameterizedType()) {
|
||||
return extractTypeName(((ParameterizedType) type).getType());
|
||||
} else if (type.isSimpleType()) {
|
||||
return ((SimpleType) type).getName().getFullyQualifiedName();
|
||||
} else if (type.isQualifiedType()) {
|
||||
return ((QualifiedType) type).getName().getFullyQualifiedName();
|
||||
}
|
||||
return type.toString();
|
||||
}
|
||||
|
||||
public MethodDeclaration findMethodDeclaration(TypeDeclaration td, String methodName, boolean includeSuper) {
|
||||
for (MethodDeclaration method : td.getMethods()) {
|
||||
if (method.getName().getIdentifier().equals(methodName)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
if (includeSuper) {
|
||||
String superFqn = getSuperclassFqn(td);
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = getTypeDeclaration(superFqn);
|
||||
if (superTd != null) {
|
||||
return findMethodDeclaration(superTd, methodName, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check interfaces
|
||||
for (Object interfaceTypeObj : td.superInterfaceTypes()) {
|
||||
Type interfaceType = (Type) interfaceTypeObj;
|
||||
String interfaceName = extractTypeName(interfaceType);
|
||||
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, (CompilationUnit) td.getRoot());
|
||||
if (interfaceTd != null) {
|
||||
MethodDeclaration found = findMethodDeclaration(interfaceTd, methodName, true);
|
||||
if (found != null) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getFqn(TypeDeclaration td) {
|
||||
StringBuilder sb = new StringBuilder(td.getName().getIdentifier());
|
||||
ASTNode current = td.getParent();
|
||||
|
||||
Reference in New Issue
Block a user