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 dc2bdb2..63bf389 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 @@ -120,7 +120,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine { String targetMethod = tp.getClassName() + "." + tp.getMethodName(); List> allPaths = new ArrayList<>(); for (String sMethod : startMethods) { - allPaths.addAll(pathFinder.findAllPaths(sMethod, targetMethod, callGraph, new HashSet<>())); + allPaths.addAll(pathFinder.findAllPaths( + sMethod, targetMethod, callGraph, new HashSet<>(), + pathBindingEvaluator, initialBindings)); } Set> uniquePaths = new LinkedHashSet<>(allPaths); for (List path : uniquePaths) { diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java index 288969e..77587e5 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java @@ -56,6 +56,21 @@ public class CallGraphPathFinder { return findAllPaths(start, target, graph, visited, new boolean[]{false}); } + public List> findAllPaths( + String start, + String target, + Map> graph, + Set visited, + PathBindingEvaluator bindingEvaluator, + Map initialBindings) { + if (bindingEvaluator == null) { + return findAllPaths(start, target, graph, visited); + } + Map bindings = initialBindings == null ? new HashMap<>() : new HashMap<>(initialBindings); + return findAllPathsConstrained( + start, target, graph, visited, new boolean[]{false}, bindingEvaluator, bindings); + } + public List> findAllPaths(String start, String target, Map> graph, Set visited, boolean[] parentHitCycle) { List> result = new ArrayList<>(); java.util.Map.Entry cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(start, target); @@ -175,6 +190,121 @@ public class CallGraphPathFinder { return result; } + private List> findAllPathsConstrained( + String start, + String target, + Map> graph, + Set visited, + boolean[] parentHitCycle, + PathBindingEvaluator bindingEvaluator, + Map bindings) { + List> result = new ArrayList<>(); + if (start.equals(target)) { + result.add(new ArrayList<>(List.of(start))); + return result; + } + if (!visited.add(start)) { + parentHitCycle[0] = true; + return result; + } + + List neighbors = graph.get(start); + if (neighbors != null) { + for (CallEdge edge : neighbors) { + String neighbor = edge.getTargetMethod(); + if (neighbor.startsWith("java.") || neighbor.startsWith("javax.") || neighbor.startsWith("jakarta.") || neighbor.startsWith("org.springframework.")) { + continue; + } + + String resolvedTarget = neighbor.equals(target) || isHeuristicMatch(neighbor, target) ? target : neighbor; + List hopPath = new ArrayList<>(List.of(start, resolvedTarget)); + Map branchBindings = new HashMap<>(bindings); + if (!bindingEvaluator.tryTraverseEdge( + start, resolvedTarget, edge, hopPath, 1, graph, this, branchBindings)) { + continue; + } + + if (resolvedTarget.equals(target)) { + result.add(hopPath); + } else { + List> subPaths = findAllPathsConstrained( + neighbor, target, graph, visited, parentHitCycle, bindingEvaluator, branchBindings); + for (List subPath : subPaths) { + if (subPath.isEmpty() || subPath.get(0).equals(start)) { + continue; + } + List path = new ArrayList<>(); + path.add(start); + path.addAll(subPath); + result.add(path); + } + } + } + } + + if (result.isEmpty() && start.contains(".")) { + result.addAll(findInheritanceFallbackPaths( + start, target, graph, visited, parentHitCycle, bindingEvaluator, bindings)); + } + + visited.remove(start); + return result; + } + + private List> findInheritanceFallbackPaths( + String start, + String target, + Map> graph, + Set visited, + boolean[] parentHitCycle, + PathBindingEvaluator bindingEvaluator, + Map bindings) { + List> result = new ArrayList<>(); + String className = start.substring(0, start.lastIndexOf('.')); + String methodName = start.substring(start.lastIndexOf('.') + 1); + if (className.contains("<")) { + className = className.substring(0, className.indexOf('<')); + } + + TypeDeclaration td = context.getTypeDeclaration(className); + if (td != null) { + String superclass = context.getSuperclassFqn(td); + if (superclass != null) { + String superMethod = superclass + "." + methodName; + if (graph.containsKey(superMethod)) { + List> superPaths = findAllPathsConstrained( + superMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings); + for (List superPath : superPaths) { + List path = new ArrayList<>(); + path.add(start); + path.addAll(superPath); + result.add(path); + } + } + } + } + + if (result.isEmpty()) { + List impls = context.getImplementations(className); + if (impls != null) { + for (String impl : impls) { + String implMethod = impl + "." + methodName; + if (graph.containsKey(implMethod)) { + List> implPaths = findAllPathsConstrained( + implMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings); + for (List implPath : implPaths) { + List path = new ArrayList<>(); + path.add(start); + path.addAll(implPath); + result.add(path); + } + } + } + } + } + return result; + } + public String extractContextMachineId(List path, Map> callGraph) { for (String node : path) { List edges = callGraph.get(node); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/PathBindingEvaluator.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/PathBindingEvaluator.java index 6c29533..3895673 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/PathBindingEvaluator.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/PathBindingEvaluator.java @@ -83,6 +83,31 @@ public class PathBindingEvaluator { return isPathCompatible(path, callGraph, pathFinder, Map.of()); } + /** + * Returns {@code true} when {@code caller}→{@code callee} via {@code edge} satisfies the edge constraint + * under {@code bindings}; on success, {@code bindings} is updated with propagated parameter values. + */ + public boolean tryTraverseEdge( + String caller, + String callee, + CallEdge edge, + List pathWithCallee, + int calleeIndex, + Map> callGraph, + CallGraphPathFinder pathFinder, + Map bindings) { + if (edge.getConstraint() != null + && !edge.getConstraint().isBlank() + && !BooleanConstraintEvaluator.isCompatibleWithBindings(edge.getConstraint(), bindings)) { + return false; + } + if (variableTracer == null || typeResolver == null) { + return true; + } + enrichBindings(caller, callee, edge, pathWithCallee, calleeIndex, callGraph, pathFinder, bindings); + return true; + } + private void enrichBindings( String caller, String target, diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinderTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinderTest.java index c631ffa..742ca4f 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinderTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinderTest.java @@ -78,4 +78,40 @@ class CallGraphPathFinderTest { "com.acme.End.finish" ); } + + @Test + void shouldPruneBranchesDuringSearchWhenConstraintsPresent() { + Map> graph = new HashMap<>(); + graph.put("com.acme.Start.run", List.of( + new CallEdge("com.acme.BranchA.step", List.of(), null, "command == \"PAY\""), + new CallEdge("com.acme.BranchB.step", List.of(), null, "command == \"SHIP\"") + )); + graph.put("com.acme.BranchA.step", List.of( + new CallEdge("com.acme.End.finish", List.of()) + )); + graph.put("com.acme.BranchB.step", List.of( + new CallEdge("com.acme.End.finish", List.of()) + )); + + CallGraphPathFinder pathFinder = new CallGraphPathFinder(new CodebaseContext()); + PathBindingEvaluator evaluator = new PathBindingEvaluator( + new CodebaseContext(), null, null, null); + + List> unconstrained = pathFinder.findAllPaths( + "com.acme.Start.run", "com.acme.End.finish", graph, new HashSet<>()); + assertThat(unconstrained).hasSize(2); + + List> payOnly = pathFinder.findAllPaths( + "com.acme.Start.run", + "com.acme.End.finish", + graph, + new HashSet<>(), + evaluator, + Map.of("command", "PAY")); + assertThat(payOnly).hasSize(1); + assertThat(payOnly.get(0)).containsExactly( + "com.acme.Start.run", + "com.acme.BranchA.step", + "com.acme.End.finish"); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/PathBindingEvaluatorTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/PathBindingEvaluatorTest.java index 69edea5..621a187 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/PathBindingEvaluatorTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/PathBindingEvaluatorTest.java @@ -65,6 +65,9 @@ class PathBindingEvaluatorTest { Map> graph = engine.buildCallGraph(); CallGraphPathFinder pathFinder = new CallGraphPathFinder(context); + PathBindingEvaluator evaluator = new PathBindingEvaluator( + context, engine.variableTracer, context.getConstantResolver(), engine.typeResolver); + List> rawPaths = pathFinder.findAllPaths( "com.example.ApiController.pay", "com.example.CentralDispatcher.fire", @@ -72,8 +75,15 @@ class PathBindingEvaluatorTest { new java.util.HashSet<>()); assertThat(rawPaths).hasSize(2); - PathBindingEvaluator evaluator = new PathBindingEvaluator( - context, engine.variableTracer, context.getConstantResolver(), engine.typeResolver); + List> constrainedPaths = pathFinder.findAllPaths( + "com.example.ApiController.pay", + "com.example.CentralDispatcher.fire", + graph, + new java.util.HashSet<>(), + evaluator, + Map.of()); + assertThat(constrainedPaths).hasSize(1); + assertThat(constrainedPaths.get(0)).contains("com.example.CentralDispatcher.orderPay"); List dispatchEdges = graph.get("com.example.CentralDispatcher.dispatch"); assertThat(dispatchEdges.stream().map(CallEdge::getConstraint).filter(c -> c != null && !c.isBlank()))