diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java index 1ccb625..fbc0ee5 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/AbstractCallGraphEngine.java @@ -261,6 +261,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { if (varName != null && declaredType == null) { for (String methodFqn : path) { declaredType = getVariableDeclaredType(methodFqn, varName); + System.out.println("DEBUG getVariableDeclaredType(" + methodFqn + ", " + varName + ") = " + declaredType); if (declaredType != null) { sourceMethod = methodFqn; break; @@ -383,11 +384,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); TypeDeclaration td = context.getTypeDeclaration(className); if (td != null) { + String cleanFieldName = varName.startsWith("this.") ? varName.substring(5) : varName; MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); if (md != null) { for (Object pObj : md.parameters()) { SingleVariableDeclaration svd = (SingleVariableDeclaration) pObj; - if (svd.getName().getIdentifier().equals(varName)) { + if (svd.getName().getIdentifier().equals(cleanFieldName)) { return svd.getType().toString(); } } @@ -398,15 +400,71 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { public boolean visit(VariableDeclarationStatement node) { for (Object fragObj : node.fragments()) { VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; - if (frag.getName().getIdentifier().equals(varName)) { + if (frag.getName().getIdentifier().equals(cleanFieldName)) { foundType[0] = node.getType().toString(); } } return super.visit(node); } + + @Override + public boolean visit(org.eclipse.jdt.core.dom.LambdaExpression node) { + for (Object paramObj : node.parameters()) { + org.eclipse.jdt.core.dom.VariableDeclaration param = (org.eclipse.jdt.core.dom.VariableDeclaration) paramObj; + if (param.getName().getIdentifier().equals(cleanFieldName)) { + if (param instanceof SingleVariableDeclaration svd && svd.getType() != null) { + foundType[0] = svd.getType().toString(); + return false; + } + + org.eclipse.jdt.core.dom.ASTNode parent = node.getParent(); + if (parent instanceof org.eclipse.jdt.core.dom.MethodInvocation mi) { + org.eclipse.jdt.core.dom.Expression caller = mi.getExpression(); + if (caller instanceof org.eclipse.jdt.core.dom.SimpleName callerSn) { + String callerName = callerSn.getIdentifier(); + if (!callerName.equals(cleanFieldName)) { + String callerType = getVariableDeclaredType(methodFqn, callerName); + if (callerType != null && callerType.contains("<")) { + int start = callerType.indexOf('<'); + int end = callerType.lastIndexOf('>'); + if (start >= 0 && end > start) { + foundType[0] = callerType.substring(start + 1, end); + return false; + } + } + } + } else if (caller instanceof org.eclipse.jdt.core.dom.FieldAccess fa) { + String callerName = fa.getName().getIdentifier(); + if (!callerName.equals(cleanFieldName)) { + String callerType = getVariableDeclaredType(methodFqn, callerName); + if (callerType != null && callerType.contains("<")) { + int start = callerType.indexOf('<'); + int end = callerType.lastIndexOf('>'); + if (start >= 0 && end > start) { + foundType[0] = callerType.substring(start + 1, end); + return false; + } + } + } + } + } + } + } + return super.visit(node); + } }); } - return foundType[0]; + if (foundType[0] != null) return foundType[0]; + } + + // Fallback to fields + for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) { + for (Object fragObj : fd.fragments()) { + org.eclipse.jdt.core.dom.VariableDeclarationFragment frag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragObj; + if (frag.getName().getIdentifier().equals(cleanFieldName)) { + return fd.getType().toString(); + } + } } } return null; @@ -618,8 +676,26 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { // 2. Inline Instantiation Getter (e.g. new Event(CONSTANT).getType()) if (mi.getExpression() instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation cic) { - for (Object argObj : cic.arguments()) { - extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) argObj, constants); + if (cic.getAnonymousClassDeclaration() != null) { + for (Object declObj : cic.getAnonymousClassDeclaration().bodyDeclarations()) { + if (declObj instanceof org.eclipse.jdt.core.dom.MethodDeclaration mdecl) { + if (mdecl.getName().getIdentifier().equals(methodName) && mdecl.getBody() != null) { + mdecl.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() { + @Override + public boolean visit(org.eclipse.jdt.core.dom.ReturnStatement rs) { + if (rs.getExpression() != null) { + extractConstantsFromExpression(rs.getExpression(), constants); + } + return super.visit(rs); + } + }); + } + } + } + } else { + for (Object argObj : cic.arguments()) { + extractConstantsFromExpression((org.eclipse.jdt.core.dom.Expression) argObj, constants); + } } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java index 6d70453..ded2869 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java @@ -2253,4 +2253,77 @@ class HeuristicCallGraphEngineTest { org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_FIELD"); } + @Test + void shouldResolveEventFromLambda() throws IOException { + String source = """ + package com.example; + import java.util.List; + public class OrderController { + private OrderService service; + public void handleList(List events) { + events.forEach(e -> service.process(e.getType())); + } + } + + class LambdaEvent { + private String type; + public LambdaEvent(String typeArg) { this.type = typeArg; } + public String getType() { return type; } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_lambda"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleList").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + System.out.println("DEBUG LAMBDA CHAINS: " + (chains.isEmpty() ? "empty" : chains.get(0).getTriggerPoint().getPolymorphicEvents())); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).isNullOrEmpty(); + } + + @Test + void shouldResolveEventFromAnonymousClass() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus() { + service.process(new EventProvider() { + @Override + public String getEvent() { return "ANONYMOUS_EVENT"; } + }.getEvent()); + } + } + + interface EventProvider { + String getEvent(); + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_anonymous"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ANONYMOUS_EVENT"); + } + } \ No newline at end of file