From cbb00e8a0f4491a381ddfcfbcf7050d17f99b755 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Sun, 21 Jun 2026 18:36:37 +0200 Subject: [PATCH] Fix AST constructor tracking gap for variables and fields --- .../service/AbstractCallGraphEngine.java | 54 +++++++- .../service/HeuristicCallGraphEngineTest.java | 118 ++++++++++++++++++ .../PolymorphicStateMachineConfiguration.json | 16 +-- 3 files changed, 172 insertions(+), 16 deletions(-) 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 9a620bd..1ccb625 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 @@ -144,6 +144,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } String tracedVar = traceLocalVariable(entryMethod, currentParamName); + System.out.println("DEBUG tracedVar: " + tracedVar + " for currentParamName: " + currentParamName); if (tracedVar != null && !tracedVar.equals(currentParamName)) { String[] extractedFinalTraced = extractMethodSuffix(tracedVar, methodSuffix); tracedVar = extractedFinalTraced[0]; @@ -696,9 +697,55 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { if (expr instanceof SimpleName sn) { return sn.getIdentifier(); } - return expr.toString(); + return initializer[0].toString(); } } + + // If local variable not found, check field assignments + String cleanFieldName = varName.startsWith("this.") ? varName.substring(5) : varName; + final Expression[] fieldInitializer = new Expression[1]; + ASTVisitor fieldVisitor = new ASTVisitor() { + @Override + public boolean visit(VariableDeclarationFragment node) { + if (node.getName().getIdentifier().equals(cleanFieldName) && node.getInitializer() != null) { + fieldInitializer[0] = node.getInitializer(); + } + return super.visit(node); + } + @Override + public boolean visit(Assignment node) { + Expression left = node.getLeftHandSide(); + if ((left instanceof SimpleName && ((SimpleName) left).getIdentifier().equals(cleanFieldName)) || + (left instanceof FieldAccess && ((FieldAccess) left).getName().getIdentifier().equals(cleanFieldName))) { + fieldInitializer[0] = node.getRightHandSide(); + } + return super.visit(node); + } + }; + + for (org.eclipse.jdt.core.dom.FieldDeclaration fd : td.getFields()) { + for (Object fragObj : fd.fragments()) { + VariableDeclarationFragment frag = (VariableDeclarationFragment) fragObj; + if (frag.getName().getIdentifier().equals(cleanFieldName) && frag.getInitializer() != null) { + fieldInitializer[0] = frag.getInitializer(); + } + } + } + if (fieldInitializer[0] != null) return fieldInitializer[0].toString(); + + for (MethodDeclaration classMd : td.getMethods()) { + if (classMd.isConstructor() && classMd.getBody() != null) { + classMd.getBody().accept(fieldVisitor); + } + } + if (fieldInitializer[0] != null) return fieldInitializer[0].toString(); + + for (MethodDeclaration classMd : td.getMethods()) { + if (!classMd.isConstructor() && classMd.getBody() != null) { + classMd.getBody().accept(fieldVisitor); + } + } + if (fieldInitializer[0] != null) return fieldInitializer[0].toString(); } return null; } @@ -795,8 +842,11 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { } protected String[] extractMethodSuffix(String paramName, String currentSuffix) { - int dotIndex = paramName.indexOf('.'); + int dotIndex = paramName.lastIndexOf('.'); if (dotIndex > 0 && dotIndex + 1 < paramName.length() && Character.isLowerCase(paramName.charAt(dotIndex + 1))) { + if (paramName.substring(0, dotIndex).equals("this") && !paramName.endsWith("()")) { + return new String[] { paramName, currentSuffix }; + } return new String[] { paramName.substring(0, dotIndex), paramName.substring(dotIndex) + currentSuffix }; } return new String[] { paramName, currentSuffix }; 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 4376024..6d70453 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 @@ -2134,5 +2134,123 @@ class HeuristicCallGraphEngineTest { org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST"); } + @Test + void shouldResolveEventFromConstructorAssignment() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus() { + EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123); + service.process(e.getType()); + } + } + + class EventWithConstructorArg { + private String type; + public EventWithConstructorArg(String typeArg, int other) { + this.type = typeArg; + } + public String getType() { return type; } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_constructor_assignment"); + 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("EVENT_FROM_CONSTRUCTOR"); + } + + @Test + void shouldPrioritizeSetterOverConstructor() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus() { + EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123); + e.setType("EVENT_FROM_SETTER"); + service.process(e.getType()); + } + } + + class EventWithConstructorArg { + private String type; + public EventWithConstructorArg(String typeArg, int other) { + this.type = typeArg; + } + public void setType(String t) { this.type = t; } + public String getType() { return type; } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_setter_priority"); + 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("EVENT_FROM_SETTER"); + } + @Test + void shouldResolveEventFromFieldConstructorAssignment() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + private EventWithConstructorArg e; + public OrderController() { + this.e = new EventWithConstructorArg("EVENT_FROM_FIELD", 123); + } + public void handleStatus() { + service.process(this.e.getType()); + } + } + + class EventWithConstructorArg { + private String type; + public EventWithConstructorArg(String typeArg, int other) { + this.type = typeArg; + } + public String getType() { return type; } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_field_assignment"); + 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)); + System.out.println("DEBUG 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()).containsExactlyInAnyOrder("EVENT_FROM_FIELD"); + } } \ No newline at end of file diff --git a/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json index 20d01b6..2ba364b 100644 --- a/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json +++ b/state_machine_exporter/src/test/resources/golden/PolymorphicStateMachineConfiguration/PolymorphicStateMachineConfiguration.json @@ -408,7 +408,7 @@ }, "methodChain" : [ "click.kamil.examples.statemachine.polymorphic.PolymorphicController.payList", "click.kamil.examples.statemachine.polymorphic.OrderService.processEvent" ], "triggerPoint" : { - "event" : "event", + "event" : "new PayEvent()", "className" : "click.kamil.examples.statemachine.polymorphic.OrderService", "methodName" : "processEvent", "sourceFile" : "src/main/java/click/kamil/examples/statemachine/polymorphic/OrderService.java", @@ -416,25 +416,13 @@ "stateMachineId" : null, "sourceState" : null, "lineNumber" : 13, - "polymorphicEvents" : null + "polymorphicEvents" : [ "OrderEvents.PAY" ] }, "contextMachineId" : null, "matchedTransitions" : [ { "sourceState" : "OrderStates.SUBMITTED", "targetState" : "OrderStates.PAID", "event" : "OrderEvents.PAY" - }, { - "sourceState" : "OrderStates.PAID", - "targetState" : "OrderStates.FULFILLED", - "event" : "OrderEvents.FULFILL" - }, { - "sourceState" : "OrderStates.SUBMITTED", - "targetState" : "OrderStates.CANCELED", - "event" : "OrderEvents.CANCEL" - }, { - "sourceState" : "OrderStates.PAID", - "targetState" : "OrderStates.CANCELED", - "event" : "OrderEvents.ABCD" } ] }, { "entryPoint" : {