From bf9208d5299649876bf1e23980d7ce35fb16227a Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Fri, 19 Jun 2026 21:21:44 +0200 Subject: [PATCH] better enricher attempt --- .../enricher/TransitionLinkerEnricher.java | 8 +-- .../analysis/service/CallGraphBuilder.java | 54 +++++++++++++++++++ .../TransitionLinkerEnricherTest.java | 6 +-- .../service/CallGraphBuilderTest.java | 54 +++++++++++++++++++ 4 files changed, 115 insertions(+), 7 deletions(-) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java index a7e1eda..b39baf4 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java @@ -51,10 +51,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher { if (targetVar == null && chain.getTriggerPoint() != null) { targetVar = chain.getTriggerPoint().getStateMachineId(); } - if (targetVar == null || targetVar.isEmpty()) { - // Wildcard without specific routing context is too broad, do not link - continue; - } + // We no longer hard-block wildcards without a specific routing context. + // If a project doesn't use standard SM persisters (e.g. restores state manually), + // contextMachineId will be null. We should still link the wildcard to provide SOME visibility, + // rather than completely hiding the endpoint. } if (isWildcard || smEvent.equals(triggerEvent) || triggerEvent.toLowerCase().contains(smEvent.toLowerCase()) || smEvent.toLowerCase().contains(triggerEvent.toLowerCase())) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java index ff3531c..3fb054b 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilder.java @@ -84,6 +84,15 @@ public class CallGraphBuilder { // Find parameter index in target method int paramIndex = getParameterIndex(target, currentParamName); + if (paramIndex < 0) { + // Not a parameter. Maybe it's a local variable initialized from a parameter or method call? + String tracedVar = traceLocalVariable(target, currentParamName); + if (tracedVar != null && !tracedVar.equals(currentParamName)) { + currentParamName = tracedVar; + // We must try to find paramIndex again, because the traced variable might be a parameter of this same method + paramIndex = getParameterIndex(target, currentParamName); + } + } if (paramIndex < 0) { break; // Parameter name changed or not found, stop tracing } @@ -141,6 +150,51 @@ public class CallGraphBuilder { return -1; } + private String traceLocalVariable(String methodFqn, String varName) { + if (methodFqn == null || !methodFqn.contains(".")) return null; + String className = methodFqn.substring(0, methodFqn.lastIndexOf('.')); + String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1); + TypeDeclaration td = context.getTypeDeclaration(className); + if (td != null) { + MethodDeclaration md = context.findMethodDeclaration(td, methodName, true); + if (md != null && md.getBody() != null) { + final Expression[] initializer = new Expression[1]; + md.getBody().accept(new ASTVisitor() { + @Override + public boolean visit(VariableDeclarationFragment node) { + if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) { + initializer[0] = node.getInitializer(); + } + return super.visit(node); + } + @Override + public boolean visit(Assignment node) { + if (node.getLeftHandSide() instanceof SimpleName asn && asn.getIdentifier().equals(varName)) { + initializer[0] = node.getRightHandSide(); + } + return super.visit(node); + } + }); + + if (initializer[0] != null) { + Expression expr = traceVariable(initializer[0]); + if (expr instanceof MethodInvocation mi) { + // e.g. event.getType() -> return "event.getType()" or just "event" + if (mi.getExpression() instanceof SimpleName sn) { + return sn.getIdentifier() + "." + mi.getName().getIdentifier() + "()"; + } + return mi.getName().getIdentifier() + "()"; + } + if (expr instanceof SimpleName sn) { + return sn.getIdentifier(); + } + return expr.toString(); + } + } + } + return null; + } + private String extractContextMachineId(List path, Map> callGraph) { for (String node : path) { List edges = callGraph.get(node); diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java index ba91d4f..d3600af 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricherTest.java @@ -168,7 +168,7 @@ class TransitionLinkerEnricherTest { } @Test - void shouldIgnoreWildcardWhenContextMachineIdIsMissing() { + void shouldAllowWildcardWhenContextMachineIdIsMissing() { Transition t1 = new Transition(); t1.setSourceStates(List.of(State.of("NEW", "NEW"))); t1.setTargetStates(List.of(State.of("PAID", "PAID"))); @@ -176,7 +176,7 @@ class TransitionLinkerEnricherTest { CallChain chain = CallChain.builder() .triggerPoint(TriggerPoint.builder().event("event").build()) - // Intentionally NO contextMachineId or stateMachineId + // NO contextMachineId or stateMachineId .build(); AnalysisResult result = AnalysisResult.builder() @@ -188,6 +188,6 @@ class TransitionLinkerEnricherTest { enricher.enrich(result, null, null); CallChain updatedChain = result.getMetadata().getCallChains().get(0); - assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty(); + assertThat(updatedChain.getMatchedTransitions()).hasSize(1); } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java index 7d4fe72..4ba164b 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphBuilderTest.java @@ -165,4 +165,58 @@ class CallGraphBuilderTest { CallChain chain = chains.get(0); assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CREATE"); } + + @Test + void shouldResolveTriggerPointLocalVariableAcrossChain() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(MyOrderEvent domainEvent) { + doProcessOrderEvent(domainEvent); + } + + private void doProcessOrderEvent(MyOrderEvent domainEvent) { + OrderEvents eventType = domainEvent.getType(); + service.updateOrderState(eventType); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) { + // sendEvent + } + } + + class MyOrderEvent { + public OrderEvents getType() { return OrderEvents.PAY; } + } + + enum OrderEvents { CREATE, PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test2"); + Files.writeString(tempDir.resolve("OrderConfig.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + CallGraphBuilder builder = new CallGraphBuilder(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("processOrderEvent") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.OrderService") + .methodName("updateOrderState") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("domainEvent.getType()"); + } }