A
This commit is contained in:
@@ -112,11 +112,12 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
for (String sMethod : startMethods) {
|
for (String sMethod : startMethods) {
|
||||||
if (initialBindings.isEmpty()) {
|
if (initialBindings.isEmpty()) {
|
||||||
allPaths.addAll(pathFinder.findAllPaths(
|
allPaths.addAll(pathFinder.findAllPaths(
|
||||||
sMethod, targetMethod, callGraph, new HashSet<>()));
|
sMethod, targetMethod, callGraph, new HashSet<>(),
|
||||||
|
ep.getClassName()));
|
||||||
} else {
|
} else {
|
||||||
allPaths.addAll(pathFinder.findAllPaths(
|
allPaths.addAll(pathFinder.findAllPaths(
|
||||||
sMethod, targetMethod, callGraph, new HashSet<>(),
|
sMethod, targetMethod, callGraph, new HashSet<>(),
|
||||||
pathBindingEvaluator, initialBindings));
|
pathBindingEvaluator, initialBindings, ep.getClassName()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Set<List<String>> uniquePaths = new LinkedHashSet<>(allPaths);
|
Set<List<String>> uniquePaths = new LinkedHashSet<>(allPaths);
|
||||||
|
|||||||
@@ -2,17 +2,25 @@ package click.kamil.springstatemachineexporter.analysis.service;
|
|||||||
|
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import java.util.*;
|
import org.eclipse.jdt.core.dom.Modifier;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CallGraphPathFinder {
|
public class CallGraphPathFinder {
|
||||||
private final CodebaseContext context;
|
private final CodebaseContext context;
|
||||||
private final Map<java.util.Map.Entry<String, String>, Boolean> heuristicCache = new HashMap<>();
|
private final Map<Map.Entry<String, String>, Boolean> heuristicCache = new HashMap<>();
|
||||||
|
|
||||||
private final Map<java.util.Map.Entry<String, String>, List<List<String>>> memoCache = new HashMap<>();
|
/** Memo key: start|target|concreteSelf */
|
||||||
private final Map<java.util.Map.Entry<String, String>, Boolean> unreachableCache = new HashMap<>();
|
private final Map<String, List<List<String>>> memoCache = new HashMap<>();
|
||||||
|
private final Map<String, Boolean> unreachableCache = new HashMap<>();
|
||||||
|
|
||||||
public CallGraphPathFinder(CodebaseContext context) {
|
public CallGraphPathFinder(CodebaseContext context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@@ -59,7 +67,16 @@ public class CallGraphPathFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
public List<List<String>> findAllPaths(String start, String target, Map<String, List<CallEdge>> graph, Set<String> visited) {
|
||||||
return findAllPaths(start, target, graph, visited, new boolean[]{false});
|
return findAllPaths(start, target, graph, visited, new boolean[]{false}, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<List<String>> findAllPaths(
|
||||||
|
String start,
|
||||||
|
String target,
|
||||||
|
Map<String, List<CallEdge>> graph,
|
||||||
|
Set<String> visited,
|
||||||
|
String concreteSelfFqn) {
|
||||||
|
return findAllPaths(start, target, graph, visited, new boolean[]{false}, concreteSelfFqn);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<List<String>> findAllPaths(
|
public List<List<String>> findAllPaths(
|
||||||
@@ -69,17 +86,43 @@ public class CallGraphPathFinder {
|
|||||||
Set<String> visited,
|
Set<String> visited,
|
||||||
PathBindingEvaluator bindingEvaluator,
|
PathBindingEvaluator bindingEvaluator,
|
||||||
Map<String, String> initialBindings) {
|
Map<String, String> initialBindings) {
|
||||||
|
return findAllPaths(start, target, graph, visited, bindingEvaluator, initialBindings, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<List<String>> findAllPaths(
|
||||||
|
String start,
|
||||||
|
String target,
|
||||||
|
Map<String, List<CallEdge>> graph,
|
||||||
|
Set<String> visited,
|
||||||
|
PathBindingEvaluator bindingEvaluator,
|
||||||
|
Map<String, String> initialBindings,
|
||||||
|
String concreteSelfFqn) {
|
||||||
if (bindingEvaluator == null) {
|
if (bindingEvaluator == null) {
|
||||||
return findAllPaths(start, target, graph, visited);
|
return findAllPaths(start, target, graph, visited, concreteSelfFqn);
|
||||||
}
|
}
|
||||||
Map<String, String> bindings = initialBindings == null ? new HashMap<>() : new HashMap<>(initialBindings);
|
Map<String, String> bindings = initialBindings == null ? new HashMap<>() : new HashMap<>(initialBindings);
|
||||||
return findAllPathsConstrained(
|
return findAllPathsConstrained(
|
||||||
start, target, graph, visited, new boolean[]{false}, bindingEvaluator, bindings);
|
start, target, graph, visited, new boolean[]{false}, bindingEvaluator, bindings, concreteSelfFqn);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
return findAllPaths(start, target, graph, visited, parentHitCycle, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<List<String>> findAllPaths(
|
||||||
|
String start,
|
||||||
|
String target,
|
||||||
|
Map<String, List<CallEdge>> graph,
|
||||||
|
Set<String> visited,
|
||||||
|
boolean[] parentHitCycle,
|
||||||
|
String concreteSelfFqn) {
|
||||||
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);
|
String cacheKey = memoKey(start, target, concreteSelfFqn);
|
||||||
if (unreachableCache.containsKey(cacheKey)) {
|
if (unreachableCache.containsKey(cacheKey)) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -110,17 +153,26 @@ public class CallGraphPathFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean[] localHitCycle = new boolean[]{false};
|
boolean[] localHitCycle = new boolean[]{false};
|
||||||
|
String selfAtStart = concreteSelfFqn;
|
||||||
|
|
||||||
List<CallEdge> neighbors = graph.get(start);
|
List<CallEdge> neighbors = graph.get(start);
|
||||||
if (neighbors != null) {
|
if (neighbors != null) {
|
||||||
for (CallEdge edge : neighbors) {
|
for (CallEdge edge : neighbors) {
|
||||||
String neighbor = edge.getTargetMethod();
|
String neighbor = edge.getTargetMethod();
|
||||||
if (neighbor.startsWith("java.") || neighbor.startsWith("javax.") || neighbor.startsWith("jakarta.") || neighbor.startsWith("org.springframework.")) continue;
|
if (neighbor.startsWith("java.") || neighbor.startsWith("javax.") || neighbor.startsWith("jakarta.")
|
||||||
|
|| neighbor.startsWith("org.springframework.")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!acceptVirtualHop(start, neighbor, selfAtStart)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String nextSelf = nextConcreteSelf(selfAtStart, start, neighbor);
|
||||||
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
if (neighbor.equals(target) || isHeuristicMatch(neighbor, target)) {
|
||||||
List<String> path = new ArrayList<>(List.of(start, target));
|
List<String> path = new ArrayList<>(List.of(start, target));
|
||||||
result.add(path);
|
result.add(path);
|
||||||
} else {
|
} else {
|
||||||
List<List<String>> subPaths = findAllPaths(neighbor, target, graph, visited, localHitCycle);
|
List<List<String>> subPaths =
|
||||||
|
findAllPaths(neighbor, target, graph, visited, localHitCycle, nextSelf);
|
||||||
for (List<String> subPath : subPaths) {
|
for (List<String> subPath : subPaths) {
|
||||||
List<String> path = new ArrayList<>();
|
List<String> path = new ArrayList<>();
|
||||||
path.add(start);
|
path.add(start);
|
||||||
@@ -133,18 +185,16 @@ public class CallGraphPathFinder {
|
|||||||
|
|
||||||
// Handle inheritance fallback for concrete class methods not in the graph
|
// Handle inheritance fallback for concrete class methods not in the graph
|
||||||
if (result.isEmpty() && start.contains(".")) {
|
if (result.isEmpty() && start.contains(".")) {
|
||||||
String className = start.substring(0, start.lastIndexOf('.'));
|
String className = classNameOf(start);
|
||||||
String methodName = start.substring(start.lastIndexOf('.') + 1);
|
String methodName = methodNameOf(start);
|
||||||
if (className.contains("<")) {
|
|
||||||
className = className.substring(0, className.indexOf('<'));
|
|
||||||
}
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
if (td != null) {
|
if (td != null) {
|
||||||
String superclass = context.getSuperclassFqn(td);
|
String superclass = context.getSuperclassFqn(td);
|
||||||
if (superclass != null) {
|
if (superclass != null) {
|
||||||
String superMethod = superclass + "." + methodName;
|
String superMethod = superclass + "." + methodName;
|
||||||
if (graph.containsKey(superMethod)) {
|
if (graph.containsKey(superMethod)) {
|
||||||
List<List<String>> superPaths = findAllPaths(superMethod, target, graph, visited, localHitCycle);
|
List<List<String>> superPaths =
|
||||||
|
findAllPaths(superMethod, target, graph, visited, localHitCycle, selfAtStart);
|
||||||
for (List<String> superPath : superPaths) {
|
for (List<String> superPath : superPaths) {
|
||||||
List<String> path = new ArrayList<>();
|
List<String> path = new ArrayList<>();
|
||||||
path.add(start);
|
path.add(start);
|
||||||
@@ -158,17 +208,19 @@ public class CallGraphPathFinder {
|
|||||||
|
|
||||||
// Handle implementation fallback for interfaces/superclasses (interface -> concrete class)
|
// Handle implementation fallback for interfaces/superclasses (interface -> concrete class)
|
||||||
if (result.isEmpty() && start.contains(".")) {
|
if (result.isEmpty() && start.contains(".")) {
|
||||||
String className = start.substring(0, start.lastIndexOf('.'));
|
String className = classNameOf(start);
|
||||||
String methodName = start.substring(start.lastIndexOf('.') + 1);
|
String methodName = methodNameOf(start);
|
||||||
if (className.contains("<")) {
|
|
||||||
className = className.substring(0, className.indexOf('<'));
|
|
||||||
}
|
|
||||||
List<String> impls = context.getImplementations(className);
|
List<String> impls = context.getImplementations(className);
|
||||||
if (impls != null) {
|
if (impls != null) {
|
||||||
for (String impl : impls) {
|
for (String impl : impls) {
|
||||||
|
if (!acceptImplementationFallback(impl, selfAtStart)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
String implMethod = impl + "." + methodName;
|
String implMethod = impl + "." + methodName;
|
||||||
if (graph.containsKey(implMethod)) {
|
if (graph.containsKey(implMethod)) {
|
||||||
List<List<String>> implPaths = findAllPaths(implMethod, target, graph, visited, localHitCycle);
|
String nextSelf = nextConcreteSelfForImpl(selfAtStart, impl);
|
||||||
|
List<List<String>> implPaths =
|
||||||
|
findAllPaths(implMethod, target, graph, visited, localHitCycle, nextSelf);
|
||||||
for (List<String> implPath : implPaths) {
|
for (List<String> implPath : implPaths) {
|
||||||
List<String> path = new ArrayList<>();
|
List<String> path = new ArrayList<>();
|
||||||
path.add(start);
|
path.add(start);
|
||||||
@@ -203,7 +255,8 @@ public class CallGraphPathFinder {
|
|||||||
Set<String> visited,
|
Set<String> visited,
|
||||||
boolean[] parentHitCycle,
|
boolean[] parentHitCycle,
|
||||||
PathBindingEvaluator bindingEvaluator,
|
PathBindingEvaluator bindingEvaluator,
|
||||||
Map<String, String> bindings) {
|
Map<String, String> bindings,
|
||||||
|
String concreteSelfFqn) {
|
||||||
List<List<String>> result = new ArrayList<>();
|
List<List<String>> result = new ArrayList<>();
|
||||||
if (start.equals(target)) {
|
if (start.equals(target)) {
|
||||||
result.add(new ArrayList<>(List.of(start)));
|
result.add(new ArrayList<>(List.of(start)));
|
||||||
@@ -214,11 +267,16 @@ public class CallGraphPathFinder {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String selfAtStart = concreteSelfFqn;
|
||||||
List<CallEdge> neighbors = graph.get(start);
|
List<CallEdge> neighbors = graph.get(start);
|
||||||
if (neighbors != null) {
|
if (neighbors != null) {
|
||||||
for (CallEdge edge : neighbors) {
|
for (CallEdge edge : neighbors) {
|
||||||
String neighbor = edge.getTargetMethod();
|
String neighbor = edge.getTargetMethod();
|
||||||
if (neighbor.startsWith("java.") || neighbor.startsWith("javax.") || neighbor.startsWith("jakarta.") || neighbor.startsWith("org.springframework.")) {
|
if (neighbor.startsWith("java.") || neighbor.startsWith("javax.") || neighbor.startsWith("jakarta.")
|
||||||
|
|| neighbor.startsWith("org.springframework.")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!acceptVirtualHop(start, neighbor, selfAtStart)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,11 +288,13 @@ public class CallGraphPathFinder {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String nextSelf = nextConcreteSelf(selfAtStart, start, neighbor);
|
||||||
if (resolvedTarget.equals(target)) {
|
if (resolvedTarget.equals(target)) {
|
||||||
result.add(hopPath);
|
result.add(hopPath);
|
||||||
} else {
|
} else {
|
||||||
List<List<String>> subPaths = findAllPathsConstrained(
|
List<List<String>> subPaths = findAllPathsConstrained(
|
||||||
neighbor, target, graph, visited, parentHitCycle, bindingEvaluator, branchBindings);
|
neighbor, target, graph, visited, parentHitCycle, bindingEvaluator, branchBindings,
|
||||||
|
nextSelf);
|
||||||
for (List<String> subPath : subPaths) {
|
for (List<String> subPath : subPaths) {
|
||||||
if (subPath.isEmpty() || subPath.get(0).equals(start)) {
|
if (subPath.isEmpty() || subPath.get(0).equals(start)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -250,7 +310,7 @@ public class CallGraphPathFinder {
|
|||||||
|
|
||||||
if (result.isEmpty() && start.contains(".")) {
|
if (result.isEmpty() && start.contains(".")) {
|
||||||
result.addAll(findInheritanceFallbackPaths(
|
result.addAll(findInheritanceFallbackPaths(
|
||||||
start, target, graph, visited, parentHitCycle, bindingEvaluator, bindings));
|
start, target, graph, visited, parentHitCycle, bindingEvaluator, bindings, selfAtStart));
|
||||||
}
|
}
|
||||||
|
|
||||||
visited.remove(start);
|
visited.remove(start);
|
||||||
@@ -264,13 +324,11 @@ public class CallGraphPathFinder {
|
|||||||
Set<String> visited,
|
Set<String> visited,
|
||||||
boolean[] parentHitCycle,
|
boolean[] parentHitCycle,
|
||||||
PathBindingEvaluator bindingEvaluator,
|
PathBindingEvaluator bindingEvaluator,
|
||||||
Map<String, String> bindings) {
|
Map<String, String> bindings,
|
||||||
|
String concreteSelfFqn) {
|
||||||
List<List<String>> result = new ArrayList<>();
|
List<List<String>> result = new ArrayList<>();
|
||||||
String className = start.substring(0, start.lastIndexOf('.'));
|
String className = classNameOf(start);
|
||||||
String methodName = start.substring(start.lastIndexOf('.') + 1);
|
String methodName = methodNameOf(start);
|
||||||
if (className.contains("<")) {
|
|
||||||
className = className.substring(0, className.indexOf('<'));
|
|
||||||
}
|
|
||||||
|
|
||||||
TypeDeclaration td = context.getTypeDeclaration(className);
|
TypeDeclaration td = context.getTypeDeclaration(className);
|
||||||
if (td != null) {
|
if (td != null) {
|
||||||
@@ -279,7 +337,8 @@ public class CallGraphPathFinder {
|
|||||||
String superMethod = superclass + "." + methodName;
|
String superMethod = superclass + "." + methodName;
|
||||||
if (graph.containsKey(superMethod)) {
|
if (graph.containsKey(superMethod)) {
|
||||||
List<List<String>> superPaths = findAllPathsConstrained(
|
List<List<String>> superPaths = findAllPathsConstrained(
|
||||||
superMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings);
|
superMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings,
|
||||||
|
concreteSelfFqn);
|
||||||
for (List<String> superPath : superPaths) {
|
for (List<String> superPath : superPaths) {
|
||||||
List<String> path = new ArrayList<>();
|
List<String> path = new ArrayList<>();
|
||||||
path.add(start);
|
path.add(start);
|
||||||
@@ -294,10 +353,15 @@ public class CallGraphPathFinder {
|
|||||||
List<String> impls = context.getImplementations(className);
|
List<String> impls = context.getImplementations(className);
|
||||||
if (impls != null) {
|
if (impls != null) {
|
||||||
for (String impl : impls) {
|
for (String impl : impls) {
|
||||||
|
if (!acceptImplementationFallback(impl, concreteSelfFqn)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
String implMethod = impl + "." + methodName;
|
String implMethod = impl + "." + methodName;
|
||||||
if (graph.containsKey(implMethod)) {
|
if (graph.containsKey(implMethod)) {
|
||||||
|
String nextSelf = nextConcreteSelfForImpl(concreteSelfFqn, impl);
|
||||||
List<List<String>> implPaths = findAllPathsConstrained(
|
List<List<String>> implPaths = findAllPathsConstrained(
|
||||||
implMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings);
|
implMethod, target, graph, visited, parentHitCycle, bindingEvaluator, bindings,
|
||||||
|
nextSelf);
|
||||||
for (List<String> implPath : implPaths) {
|
for (List<String> implPath : implPaths) {
|
||||||
List<String> path = new ArrayList<>();
|
List<String> path = new ArrayList<>();
|
||||||
path.add(start);
|
path.add(start);
|
||||||
@@ -311,6 +375,99 @@ public class CallGraphPathFinder {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject CHA fan-out edges that dispatch to a sibling subclass incompatible with the entry's
|
||||||
|
* concrete runtime type. Fail open when concreteSelf is absent or not itself concrete.
|
||||||
|
*/
|
||||||
|
private boolean acceptVirtualHop(String callerMethod, String targetMethod, String concreteSelfFqn) {
|
||||||
|
if (!isPruningActive(concreteSelfFqn)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!isVirtualDispatchCandidate(callerMethod, targetMethod)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String targetClass = classNameOf(targetMethod);
|
||||||
|
return targetClass != null && context.isSubtypeOrSame(concreteSelfFqn, targetClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean acceptImplementationFallback(String implClassFqn, String concreteSelfFqn) {
|
||||||
|
if (!isPruningActive(concreteSelfFqn)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return context.isSubtypeOrSame(concreteSelfFqn, implClassFqn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPruningActive(String concreteSelfFqn) {
|
||||||
|
return concreteSelfFqn != null
|
||||||
|
&& !concreteSelfFqn.isBlank()
|
||||||
|
&& context.isConcreteClassType(concreteSelfFqn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An edge from an abstract/interface declaring type into one of its subtypes/implementations
|
||||||
|
* is a virtual-dispatch fan-out (template-method hook expansion).
|
||||||
|
*/
|
||||||
|
private boolean isVirtualDispatchCandidate(String callerMethod, String targetMethod) {
|
||||||
|
String callerClass = classNameOf(callerMethod);
|
||||||
|
String targetClass = classNameOf(targetMethod);
|
||||||
|
if (callerClass == null || targetClass == null || callerClass.equals(targetClass)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TypeDeclaration callerTd = context.getTypeDeclaration(callerClass);
|
||||||
|
if (callerTd == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean callerPolymorphic = callerTd.isInterface() || Modifier.isAbstract(callerTd.getModifiers());
|
||||||
|
if (!callerPolymorphic) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (context.isSubtypeOrSame(targetClass, callerClass)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
List<String> impls = context.getImplementations(callerClass);
|
||||||
|
return impls != null && impls.contains(targetClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nextConcreteSelf(String concreteSelfFqn, String callerMethod, String targetMethod) {
|
||||||
|
if (!isVirtualDispatchCandidate(callerMethod, targetMethod)) {
|
||||||
|
return concreteSelfFqn;
|
||||||
|
}
|
||||||
|
String targetClass = classNameOf(targetMethod);
|
||||||
|
if (targetClass != null && context.isConcreteClassType(targetClass)) {
|
||||||
|
return targetClass;
|
||||||
|
}
|
||||||
|
return concreteSelfFqn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nextConcreteSelfForImpl(String concreteSelfFqn, String implClassFqn) {
|
||||||
|
if (implClassFqn != null && context.isConcreteClassType(implClassFqn)) {
|
||||||
|
return implClassFqn;
|
||||||
|
}
|
||||||
|
return concreteSelfFqn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String memoKey(String start, String target, String concreteSelfFqn) {
|
||||||
|
return start + "|" + target + "|" + Objects.toString(concreteSelfFqn, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String classNameOf(String methodFqn) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String className = methodFqn.substring(0, methodFqn.lastIndexOf('.'));
|
||||||
|
if (className.contains("<")) {
|
||||||
|
className = className.substring(0, className.indexOf('<'));
|
||||||
|
}
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String methodNameOf(String methodFqn) {
|
||||||
|
if (methodFqn == null || !methodFqn.contains(".")) {
|
||||||
|
return methodFqn;
|
||||||
|
}
|
||||||
|
return methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -332,7 +489,7 @@ public class CallGraphPathFinder {
|
|||||||
if (neighbor == null || target == null) return false;
|
if (neighbor == null || target == null) return false;
|
||||||
if (neighbor.equals(target)) return true;
|
if (neighbor.equals(target)) return true;
|
||||||
|
|
||||||
java.util.Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(neighbor, target);
|
Map.Entry<String, String> cacheKey = new java.util.AbstractMap.SimpleImmutableEntry<>(neighbor, target);
|
||||||
Boolean cached = heuristicCache.get(cacheKey);
|
Boolean cached = heuristicCache.get(cacheKey);
|
||||||
if (cached != null) return cached;
|
if (cached != null) return cached;
|
||||||
|
|
||||||
@@ -376,8 +533,10 @@ public class CallGraphPathFinder {
|
|||||||
|
|
||||||
String classNeighbor = neighbor.contains(".") ? neighbor.substring(0, neighbor.lastIndexOf('.')) : null;
|
String classNeighbor = neighbor.contains(".") ? neighbor.substring(0, neighbor.lastIndexOf('.')) : null;
|
||||||
String classTarget = target.contains(".") ? target.substring(0, target.lastIndexOf('.')) : null;
|
String classTarget = target.contains(".") ? target.substring(0, target.lastIndexOf('.')) : null;
|
||||||
String simpleClassTarget = classTarget != null && classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
|
String simpleClassTarget = classTarget != null && classTarget.contains(".")
|
||||||
String simpleClassNeighbor = classNeighbor != null && classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
|
? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
|
||||||
|
String simpleClassNeighbor = classNeighbor != null && classNeighbor.contains(".")
|
||||||
|
? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
|
||||||
|
|
||||||
if (simpleClassNeighbor != null && simpleClassTarget != null) {
|
if (simpleClassNeighbor != null && simpleClassTarget != null) {
|
||||||
String leftClass = classNeighbor != null ? classNeighbor : simpleClassNeighbor;
|
String leftClass = classNeighbor != null ? classNeighbor : simpleClassNeighbor;
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ public class CodebaseContext {
|
|||||||
private final Map<String, MethodDeclaration> methodDeclarationCache = new HashMap<>();
|
private final Map<String, MethodDeclaration> methodDeclarationCache = new HashMap<>();
|
||||||
private final Map<String, String> superclassFqnCache = new HashMap<>();
|
private final Map<String, String> superclassFqnCache = new HashMap<>();
|
||||||
private final Map<String, List<String>> implementationsCache = new HashMap<>();
|
private final Map<String, List<String>> implementationsCache = new HashMap<>();
|
||||||
|
private final Map<String, Boolean> subtypeOrSameCache = new HashMap<>();
|
||||||
private boolean inlineAccessors = true;
|
private boolean inlineAccessors = true;
|
||||||
|
|
||||||
public Map<String, Object> getCache() {
|
public Map<String, Object> getCache() {
|
||||||
@@ -107,6 +108,7 @@ public class CodebaseContext {
|
|||||||
methodDeclarationCache.clear();
|
methodDeclarationCache.clear();
|
||||||
superclassFqnCache.clear();
|
superclassFqnCache.clear();
|
||||||
classCompatibilityCache.clear();
|
classCompatibilityCache.clear();
|
||||||
|
subtypeOrSameCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> getIndexedTypeFqns() {
|
public Set<String> getIndexedTypeFqns() {
|
||||||
@@ -739,6 +741,72 @@ public class CodebaseContext {
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AST-backed subtype check: {@code true} when {@code childFqn} is the same type as
|
||||||
|
* {@code parentFqn}, or extends/implements it (transitively). Does not require JDT bindings.
|
||||||
|
*/
|
||||||
|
public boolean isSubtypeOrSame(String childFqn, String parentFqn) {
|
||||||
|
if (childFqn == null || parentFqn == null || childFqn.isBlank() || parentFqn.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (childFqn.equals(parentFqn) || areSameTypeOrUnambiguousSimpleMatch(childFqn, parentFqn)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String cacheKey = childFqn + "<:" + parentFqn;
|
||||||
|
Boolean cached = subtypeOrSameCache.get(cacheKey);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
boolean result = computeIsSubtypeOrSame(childFqn, parentFqn, new HashSet<>());
|
||||||
|
subtypeOrSameCache.put(cacheKey, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code true} when the type is a known non-abstract class (not an interface).
|
||||||
|
* Unknown types are treated as non-concrete so pathfinding fails open.
|
||||||
|
*/
|
||||||
|
public boolean isConcreteClassType(String typeFqn) {
|
||||||
|
if (typeFqn == null || typeFqn.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TypeDeclaration td = getTypeDeclaration(typeFqn);
|
||||||
|
if (td == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !td.isInterface() && !Modifier.isAbstract(td.getModifiers());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean computeIsSubtypeOrSame(String childFqn, String parentFqn, Set<String> visited) {
|
||||||
|
if (childFqn == null || !visited.add(childFqn)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (childFqn.equals(parentFqn) || areSameTypeOrUnambiguousSimpleMatch(childFqn, parentFqn)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
TypeDeclaration childTd = getTypeDeclaration(childFqn);
|
||||||
|
if (childTd == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String superclass = getSuperclassFqn(childTd);
|
||||||
|
if (superclass != null && computeIsSubtypeOrSame(superclass, parentFqn, visited)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
CompilationUnit cu = childTd.getRoot() instanceof CompilationUnit root ? root : null;
|
||||||
|
for (Object interfaceTypeObj : childTd.superInterfaceTypes()) {
|
||||||
|
if (!(interfaceTypeObj instanceof Type interfaceType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String interfaceName = extractTypeName(interfaceType);
|
||||||
|
TypeDeclaration interfaceTd = getTypeDeclaration(interfaceName, cu);
|
||||||
|
String interfaceFqn = interfaceTd != null ? getFqn(interfaceTd) : interfaceName;
|
||||||
|
if (interfaceFqn != null && computeIsSubtypeOrSame(interfaceFqn, parentFqn, visited)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveSuperclassFqn(TypeDeclaration td) {
|
private String resolveSuperclassFqn(TypeDeclaration td) {
|
||||||
String bindingSuper = null;
|
String bindingSuper = null;
|
||||||
ITypeBinding binding = td.resolveBinding();
|
ITypeBinding binding = td.resolveBinding();
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
|||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@@ -114,4 +116,106 @@ class CallGraphPathFinderTest {
|
|||||||
"com.acme.BranchA.step",
|
"com.acme.BranchA.step",
|
||||||
"com.acme.End.finish");
|
"com.acme.End.finish");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectSiblingSubclassVirtualHopsWhenConcreteSelfIsKnown() throws Exception {
|
||||||
|
Path tempDir = Files.createTempDirectory("pathfinder_concrete_self");
|
||||||
|
Files.writeString(tempDir.resolve("Listeners.java"), """
|
||||||
|
package com.example;
|
||||||
|
abstract class AbstractListener {
|
||||||
|
void processMessage() { doPMessage(); }
|
||||||
|
abstract void doPMessage();
|
||||||
|
}
|
||||||
|
class OrderListener extends AbstractListener {
|
||||||
|
void onMessage() { processMessage(); }
|
||||||
|
void doPMessage() {}
|
||||||
|
}
|
||||||
|
class DocumentListener extends AbstractListener {
|
||||||
|
void onMessage() { processMessage(); }
|
||||||
|
void doPMessage() {}
|
||||||
|
}
|
||||||
|
class StateMachine {
|
||||||
|
void sendEvent() {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);
|
||||||
|
|
||||||
|
Map<String, List<CallEdge>> graph = new HashMap<>();
|
||||||
|
graph.put("com.example.OrderListener.onMessage", List.of(
|
||||||
|
new CallEdge("com.example.AbstractListener.processMessage", List.of())));
|
||||||
|
graph.put("com.example.AbstractListener.processMessage", List.of(
|
||||||
|
new CallEdge("com.example.OrderListener.doPMessage", List.of()),
|
||||||
|
new CallEdge("com.example.DocumentListener.doPMessage", List.of())));
|
||||||
|
graph.put("com.example.OrderListener.doPMessage", List.of(
|
||||||
|
new CallEdge("com.example.StateMachine.sendEvent", List.of())));
|
||||||
|
graph.put("com.example.DocumentListener.doPMessage", List.of(
|
||||||
|
new CallEdge("com.example.StateMachine.sendEvent", List.of())));
|
||||||
|
|
||||||
|
List<List<String>> withoutSelf = pathFinder.findAllPaths(
|
||||||
|
"com.example.OrderListener.onMessage",
|
||||||
|
"com.example.StateMachine.sendEvent",
|
||||||
|
graph,
|
||||||
|
new HashSet<>());
|
||||||
|
assertThat(withoutSelf).hasSize(2);
|
||||||
|
|
||||||
|
List<List<String>> withOrderSelf = pathFinder.findAllPaths(
|
||||||
|
"com.example.OrderListener.onMessage",
|
||||||
|
"com.example.StateMachine.sendEvent",
|
||||||
|
graph,
|
||||||
|
new HashSet<>(),
|
||||||
|
"com.example.OrderListener");
|
||||||
|
assertThat(withOrderSelf).hasSize(1);
|
||||||
|
assertThat(withOrderSelf.get(0))
|
||||||
|
.containsExactly(
|
||||||
|
"com.example.OrderListener.onMessage",
|
||||||
|
"com.example.AbstractListener.processMessage",
|
||||||
|
"com.example.OrderListener.doPMessage",
|
||||||
|
"com.example.StateMachine.sendEvent")
|
||||||
|
.doesNotContain("com.example.DocumentListener.doPMessage");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAllowParentOverrideWhenConcreteSelfIsMoreSpecificSubclass() throws Exception {
|
||||||
|
Path tempDir = Files.createTempDirectory("pathfinder_parent_override");
|
||||||
|
Files.writeString(tempDir.resolve("Hierarchy.java"), """
|
||||||
|
package com.example;
|
||||||
|
abstract class AbstractListener {
|
||||||
|
void processMessage() { doPMessage(); }
|
||||||
|
abstract void doPMessage();
|
||||||
|
}
|
||||||
|
class OrderListener extends AbstractListener {
|
||||||
|
void doPMessage() {}
|
||||||
|
}
|
||||||
|
class SpecialOrderListener extends OrderListener {
|
||||||
|
void onMessage() { processMessage(); }
|
||||||
|
}
|
||||||
|
class StateMachine { void sendEvent() {} }
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
CallGraphPathFinder pathFinder = new CallGraphPathFinder(context);
|
||||||
|
|
||||||
|
Map<String, List<CallEdge>> graph = new HashMap<>();
|
||||||
|
graph.put("com.example.SpecialOrderListener.onMessage", List.of(
|
||||||
|
new CallEdge("com.example.AbstractListener.processMessage", List.of())));
|
||||||
|
graph.put("com.example.AbstractListener.processMessage", List.of(
|
||||||
|
new CallEdge("com.example.OrderListener.doPMessage", List.of()),
|
||||||
|
new CallEdge("com.example.DocumentListener.doPMessage", List.of())));
|
||||||
|
graph.put("com.example.OrderListener.doPMessage", List.of(
|
||||||
|
new CallEdge("com.example.StateMachine.sendEvent", List.of())));
|
||||||
|
|
||||||
|
List<List<String>> paths = pathFinder.findAllPaths(
|
||||||
|
"com.example.SpecialOrderListener.onMessage",
|
||||||
|
"com.example.StateMachine.sendEvent",
|
||||||
|
graph,
|
||||||
|
new HashSet<>(),
|
||||||
|
"com.example.SpecialOrderListener");
|
||||||
|
|
||||||
|
assertThat(paths).hasSize(1);
|
||||||
|
assertThat(paths.get(0)).contains("com.example.OrderListener.doPMessage");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,12 +70,13 @@ class ImplicitThisTemplateMethodCallGraphTest {
|
|||||||
.as("OrderListener.onMessage -> inherited processMessage -> OrderListener.doPMessage -> sendEvent")
|
.as("OrderListener.onMessage -> inherited processMessage -> OrderListener.doPMessage -> sendEvent")
|
||||||
.isNotEmpty();
|
.isNotEmpty();
|
||||||
assertThat(orderChains)
|
assertThat(orderChains)
|
||||||
.as("at least one path must go through this listener's hook, not only sibling subclasses")
|
.as("context-sensitive search must never walk sibling DocumentListener frames")
|
||||||
.anyMatch(chain -> chain.getMethodChain().stream()
|
.allSatisfy(chain -> assertThat(chain.getMethodChain())
|
||||||
|
.noneMatch(hop -> hop.contains("DocumentListener")));
|
||||||
|
assertThat(orderChains)
|
||||||
|
.allSatisfy(chain -> assertThat(chain.getMethodChain())
|
||||||
.anyMatch(hop -> hop.contains("OrderListener.doPMessage")));
|
.anyMatch(hop -> hop.contains("OrderListener.doPMessage")));
|
||||||
assertThat(orderChains.stream()
|
assertThat(orderChains.stream()
|
||||||
.filter(chain -> chain.getMethodChain().stream()
|
|
||||||
.anyMatch(hop -> hop.contains("OrderListener.doPMessage")))
|
|
||||||
.flatMap(chain -> {
|
.flatMap(chain -> {
|
||||||
List<String> events = chain.getTriggerPoint().getPolymorphicEvents();
|
List<String> events = chain.getTriggerPoint().getPolymorphicEvents();
|
||||||
return events == null ? java.util.stream.Stream.<String>empty() : events.stream();
|
return events == null ? java.util.stream.Stream.<String>empty() : events.stream();
|
||||||
@@ -97,11 +98,12 @@ class ImplicitThisTemplateMethodCallGraphTest {
|
|||||||
|
|
||||||
assertThat(documentChains).isNotEmpty();
|
assertThat(documentChains).isNotEmpty();
|
||||||
assertThat(documentChains)
|
assertThat(documentChains)
|
||||||
.anyMatch(chain -> chain.getMethodChain().stream()
|
.allSatisfy(chain -> assertThat(chain.getMethodChain())
|
||||||
|
.noneMatch(hop -> hop.contains("OrderListener")));
|
||||||
|
assertThat(documentChains)
|
||||||
|
.allSatisfy(chain -> assertThat(chain.getMethodChain())
|
||||||
.anyMatch(hop -> hop.contains("DocumentListener.doPMessage")));
|
.anyMatch(hop -> hop.contains("DocumentListener.doPMessage")));
|
||||||
assertThat(documentChains.stream()
|
assertThat(documentChains.stream()
|
||||||
.filter(chain -> chain.getMethodChain().stream()
|
|
||||||
.anyMatch(hop -> hop.contains("DocumentListener.doPMessage")))
|
|
||||||
.flatMap(chain -> {
|
.flatMap(chain -> {
|
||||||
List<String> events = chain.getTriggerPoint().getPolymorphicEvents();
|
List<String> events = chain.getTriggerPoint().getPolymorphicEvents();
|
||||||
return events == null ? java.util.stream.Stream.<String>empty() : events.stream();
|
return events == null ? java.util.stream.Stream.<String>empty() : events.stream();
|
||||||
|
|||||||
@@ -327,4 +327,29 @@ class CodebaseContextTest {
|
|||||||
|
|
||||||
assertThat(context.findEntryPointClasses(List.of("EnableStateMachine"))).hasSize(1);
|
assertThat(context.findEntryPointClasses(List.of("EnableStateMachine"))).hasSize(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isSubtypeOrSameShouldWalkSuperclassAndInterfaces() throws IOException {
|
||||||
|
Files.writeString(tempDir.resolve("Hierarchy.java"), """
|
||||||
|
package com.example;
|
||||||
|
interface Named {}
|
||||||
|
abstract class AbstractListener implements Named {}
|
||||||
|
class OrderListener extends AbstractListener {}
|
||||||
|
class DocumentListener extends AbstractListener {}
|
||||||
|
class SpecialOrderListener extends OrderListener {}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
assertThat(context.isSubtypeOrSame("com.example.OrderListener", "com.example.OrderListener")).isTrue();
|
||||||
|
assertThat(context.isSubtypeOrSame("com.example.OrderListener", "com.example.AbstractListener")).isTrue();
|
||||||
|
assertThat(context.isSubtypeOrSame("com.example.OrderListener", "com.example.Named")).isTrue();
|
||||||
|
assertThat(context.isSubtypeOrSame("com.example.SpecialOrderListener", "com.example.OrderListener")).isTrue();
|
||||||
|
assertThat(context.isSubtypeOrSame("com.example.OrderListener", "com.example.DocumentListener")).isFalse();
|
||||||
|
assertThat(context.isSubtypeOrSame("com.example.DocumentListener", "com.example.OrderListener")).isFalse();
|
||||||
|
assertThat(context.isConcreteClassType("com.example.OrderListener")).isTrue();
|
||||||
|
assertThat(context.isConcreteClassType("com.example.AbstractListener")).isFalse();
|
||||||
|
assertThat(context.isConcreteClassType("com.example.Named")).isFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user