Prune call-graph paths using switch constraints during search.

Propagate path bindings while exploring CallEdge constraints in CallGraphPathFinder so impossible dispatch arms are skipped before chain assembly, with inheritance fallback preserved for polymorphic resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 05:53:17 +02:00
parent 23a14d8391
commit db158200dd
5 changed files with 206 additions and 3 deletions

View File

@@ -120,7 +120,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
List<List<String>> 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<List<String>> uniquePaths = new LinkedHashSet<>(allPaths);
for (List<String> path : uniquePaths) {

View File

@@ -56,6 +56,21 @@ public class CallGraphPathFinder {
return findAllPaths(start, target, graph, visited, new boolean[]{false});
}
public List<List<String>> findAllPaths(
String start,
String target,
Map<String, List<CallEdge>> graph,
Set<String> visited,
PathBindingEvaluator bindingEvaluator,
Map<String, String> initialBindings) {
if (bindingEvaluator == null) {
return findAllPaths(start, target, graph, visited);
}
Map<String, String> bindings = initialBindings == null ? new HashMap<>() : new HashMap<>(initialBindings);
return findAllPathsConstrained(
start, target, graph, visited, new boolean[]{false}, bindingEvaluator, bindings);
}
public List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited, boolean[] parentHitCycle) {
List<List<String>> result = new ArrayList<>();
java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(start, target);
@@ -175,6 +190,121 @@ public class CallGraphPathFinder {
return result;
}
private List<List<String>> findAllPathsConstrained(
String start,
String target,
Map<String, List<CallEdge>> graph,
Set<String> visited,
boolean[] parentHitCycle,
PathBindingEvaluator bindingEvaluator,
Map<String, String> bindings) {
List<List<String>> 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<CallEdge> 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<String> hopPath = new ArrayList<>(List.of(start, resolvedTarget));
Map<String, String> 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<List<String>> subPaths = findAllPathsConstrained(
neighbor, target, graph, visited, parentHitCycle, bindingEvaluator, branchBindings);
for (List<String> subPath : subPaths) {
if (subPath.isEmpty() || subPath.get(0).equals(start)) {
continue;
}
List<String> 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<List<String>> findInheritanceFallbackPaths(
String start,
String target,
Map<String, List<CallEdge>> graph,
Set<String> visited,
boolean[] parentHitCycle,
PathBindingEvaluator bindingEvaluator,
Map<String, String> bindings) {
List<List<String>> 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<List<String>> superPaths = findAllPathsConstrained(
superMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings);
for (List<String> superPath : superPaths) {
List<String> path = new ArrayList<>();
path.add(start);
path.addAll(superPath);
result.add(path);
}
}
}
}
if (result.isEmpty()) {
List<String> impls = context.getImplementations(className);
if (impls != null) {
for (String impl : impls) {
String implMethod = impl + "." + methodName;
if (graph.containsKey(implMethod)) {
List<List<String>> implPaths = findAllPathsConstrained(
implMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings);
for (List<String> implPath : implPaths) {
List<String> path = new ArrayList<>();
path.add(start);
path.addAll(implPath);
result.add(path);
}
}
}
}
}
return result;
}
public String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
for (String node : path) {
List<CallEdge> edges = callGraph.get(node);

View File

@@ -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<String> pathWithCallee,
int calleeIndex,
Map<String, List<CallEdge>> callGraph,
CallGraphPathFinder pathFinder,
Map<String, String> 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,

View File

@@ -78,4 +78,40 @@ class CallGraphPathFinderTest {
"com.acme.End.finish"
);
}
@Test
void shouldPruneBranchesDuringSearchWhenConstraintsPresent() {
Map<String, List<CallEdge>> 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<List<String>> unconstrained = pathFinder.findAllPaths(
"com.acme.Start.run", "com.acme.End.finish", graph, new HashSet<>());
assertThat(unconstrained).hasSize(2);
List<List<String>> 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");
}
}

View File

@@ -65,6 +65,9 @@ class PathBindingEvaluatorTest {
Map<String, List<CallEdge>> graph = engine.buildCallGraph();
CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);
PathBindingEvaluator evaluator = new PathBindingEvaluator(
context, engine.variableTracer, context.getConstantResolver(), engine.typeResolver);
List<List<String>> 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<List<String>> 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<CallEdge> dispatchEdges = graph.get("com.example.CentralDispatcher.dispatch");
assertThat(dispatchEdges.stream().map(CallEdge::getConstraint).filter(c -> c != null && !c.isBlank()))