inhertiance

This commit is contained in:
2026-06-17 21:22:03 +02:00
parent 566a814671
commit 0c9e8de310
20 changed files with 324 additions and 8016 deletions

View File

@@ -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) {

View File

@@ -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)) {

View File

@@ -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";
}

View File

@@ -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 "";
}

View File

@@ -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();

View File

@@ -0,0 +1,90 @@
package click.kamil.springstatemachineexporter.analysis;
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.service.CallGraphBuilder;
import click.kamil.springstatemachineexporter.analysis.service.GenericEventDetector;
import click.kamil.springstatemachineexporter.analysis.service.SpringMvcDetector;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class InheritedEventDetectionTest {
@Test
void testInheritedMessageBuilderEvent(@TempDir Path tempDir) throws IOException {
Path srcDir = tempDir.resolve("src/main/java/com/example");
Files.createDirectories(srcDir);
// 1. Abstract Base Controller with protected method using MessageBuilder
Files.writeString(srcDir.resolve("BaseController.java"),
"package com.example;\n" +
"import org.springframework.messaging.support.MessageBuilder;\n" +
"import org.springframework.statemachine.StateMachine;\n" +
"public abstract class BaseController {\n" +
" protected StateMachine<String, String> stateMachine;\n" +
" \n" +
" protected void send(String event) {\n" +
" stateMachine.sendEvent(MessageBuilder.withPayload(event).build());\n" +
" }\n" +
"}");
// 2. Child Controller calling super.send()
Files.writeString(srcDir.resolve("ChildController.java"),
"package com.example;\n" +
"import org.springframework.web.bind.annotation.*;\n" +
"@RestController\n" +
"public class ChildController extends BaseController {\n" +
" \n" +
" @GetMapping(\"/trigger\")\n" +
" public void trigger() {\n" +
" super.send(\"MY_EVENT\");\n" +
" }\n" +
"}");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
// Detect Entry Points
SpringMvcDetector mvcDetector = new SpringMvcDetector(context, new ConstantResolver());
TypeDeclaration childTd = context.getTypeDeclaration("com.example.ChildController");
CompilationUnit childCu = (CompilationUnit) childTd.getRoot();
List<EntryPoint> entryPoints = mvcDetector.detect(childCu);
assertThat(entryPoints).hasSize(1);
EntryPoint ep = entryPoints.get(0);
// Detect Trigger Points
GenericEventDetector eventDetector = new GenericEventDetector(context, new ConstantResolver(), List.of());
TypeDeclaration baseTd = context.getTypeDeclaration("com.example.BaseController");
CompilationUnit baseCu = (CompilationUnit) baseTd.getRoot();
List<TriggerPoint> triggers = eventDetector.detect(baseCu);
assertThat(triggers).describedAs("Should detect sendEvent in BaseController").hasSize(1);
TriggerPoint tp = triggers.get(0);
assertThat(tp.getEvent()).isEqualTo("event");
// Build Call Chains
CallGraphBuilder callGraphBuilder = new CallGraphBuilder(context);
List<CallChain> chains = callGraphBuilder.findChains(entryPoints, triggers);
assertThat(chains).describedAs("Should link ChildController.trigger to BaseController.send").hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getMethodChain()).containsExactly(
"com.example.ChildController.trigger",
"com.example.BaseController.send"
);
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("MY_EVENT");
}
}