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:
@@ -120,7 +120,9 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
String targetMethod = tp.getClassName() + "." + tp.getMethodName();
|
||||||
List<List<String>> allPaths = new ArrayList<>();
|
List<List<String>> allPaths = new ArrayList<>();
|
||||||
for (String sMethod : startMethods) {
|
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);
|
Set<List<String>> uniquePaths = new LinkedHashSet<>(allPaths);
|
||||||
for (List<String> path : uniquePaths) {
|
for (List<String> path : uniquePaths) {
|
||||||
|
|||||||
@@ -56,6 +56,21 @@ public class CallGraphPathFinder {
|
|||||||
return findAllPaths(start, target, graph, visited, new boolean[]{false});
|
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) {
|
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<>();
|
List<List<String>> result = new ArrayList<>();
|
||||||
java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(start, target);
|
java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(start, target);
|
||||||
@@ -175,6 +190,121 @@ public class CallGraphPathFinder {
|
|||||||
return result;
|
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) {
|
public String extractContextMachineId(List<String> path, Map<String, List<CallEdge>> callGraph) {
|
||||||
for (String node : path) {
|
for (String node : path) {
|
||||||
List<CallEdge> edges = callGraph.get(node);
|
List<CallEdge> edges = callGraph.get(node);
|
||||||
|
|||||||
@@ -83,6 +83,31 @@ public class PathBindingEvaluator {
|
|||||||
return isPathCompatible(path, callGraph, pathFinder, Map.of());
|
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(
|
private void enrichBindings(
|
||||||
String caller,
|
String caller,
|
||||||
String target,
|
String target,
|
||||||
|
|||||||
@@ -78,4 +78,40 @@ class CallGraphPathFinderTest {
|
|||||||
"com.acme.End.finish"
|
"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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ class PathBindingEvaluatorTest {
|
|||||||
Map<String, List<CallEdge>> graph = engine.buildCallGraph();
|
Map<String, List<CallEdge>> graph = engine.buildCallGraph();
|
||||||
|
|
||||||
CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);
|
CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);
|
||||||
|
PathBindingEvaluator evaluator = new PathBindingEvaluator(
|
||||||
|
context, engine.variableTracer, context.getConstantResolver(), engine.typeResolver);
|
||||||
|
|
||||||
List<List<String>> rawPaths = pathFinder.findAllPaths(
|
List<List<String>> rawPaths = pathFinder.findAllPaths(
|
||||||
"com.example.ApiController.pay",
|
"com.example.ApiController.pay",
|
||||||
"com.example.CentralDispatcher.fire",
|
"com.example.CentralDispatcher.fire",
|
||||||
@@ -72,8 +75,15 @@ class PathBindingEvaluatorTest {
|
|||||||
new java.util.HashSet<>());
|
new java.util.HashSet<>());
|
||||||
assertThat(rawPaths).hasSize(2);
|
assertThat(rawPaths).hasSize(2);
|
||||||
|
|
||||||
PathBindingEvaluator evaluator = new PathBindingEvaluator(
|
List<List<String>> constrainedPaths = pathFinder.findAllPaths(
|
||||||
context, engine.variableTracer, context.getConstantResolver(), engine.typeResolver);
|
"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");
|
List<CallEdge> dispatchEdges = graph.get("com.example.CentralDispatcher.dispatch");
|
||||||
assertThat(dispatchEdges.stream().map(CallEdge::getConstraint).filter(c -> c != null && !c.isBlank()))
|
assertThat(dispatchEdges.stream().map(CallEdge::getConstraint).filter(c -> c != null && !c.isBlank()))
|
||||||
|
|||||||
Reference in New Issue
Block a user