Resolve inheritance call targets and cap polymorphic event transitions.
Add InheritanceCallTargetResolver for super/this/implicit dispatch across call-graph engines, apply transition-ceiling to polymorphic events in MachineEnumCanonicalizer, and cover JDT parity, deep hierarchy, and pipeline behavior with new tests plus golden update. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -95,6 +95,34 @@ public final class EnumMemberPredicateEvaluator {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when every predicate method in the constraint exists on the enum (or its interfaces).
|
||||
* Used to distinguish inconclusive filtering from an intentional empty result.
|
||||
*/
|
||||
public static boolean hasResolvablePredicateMethods(
|
||||
String constraint,
|
||||
String enumTypeFqn,
|
||||
CodebaseContext context) {
|
||||
List<PredicateCall> predicates = extractPredicateCalls(constraint);
|
||||
if (predicates.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
EnumDeclaration enumDecl = findEnumDeclaration(enumTypeFqn, context);
|
||||
if (enumDecl == null) {
|
||||
return false;
|
||||
}
|
||||
for (PredicateCall predicate : predicates) {
|
||||
MethodDeclaration method = findParameterlessMethod(enumDecl, predicate.methodName());
|
||||
if (method == null && context != null) {
|
||||
method = findInterfaceParameterlessMethod(enumDecl, predicate.methodName(), context);
|
||||
}
|
||||
if (method == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean satisfiesPredicates(
|
||||
String canonicalConstantFqn,
|
||||
List<PredicateCall> predicates,
|
||||
|
||||
@@ -101,6 +101,14 @@ public final class MachineEnumCanonicalizer {
|
||||
List<String> polymorphicEvents,
|
||||
String machineEventTypeFqn,
|
||||
CodebaseContext context) {
|
||||
return expandSymbolicPolymorphicEvents(polymorphicEvents, machineEventTypeFqn, context, null);
|
||||
}
|
||||
|
||||
public static List<String> expandSymbolicPolymorphicEvents(
|
||||
List<String> polymorphicEvents,
|
||||
String machineEventTypeFqn,
|
||||
CodebaseContext context,
|
||||
List<Transition> machineTransitions) {
|
||||
if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
|
||||
return polymorphicEvents;
|
||||
}
|
||||
@@ -108,6 +116,8 @@ public final class MachineEnumCanonicalizer {
|
||||
return polymorphicEvents;
|
||||
}
|
||||
|
||||
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, machineEventTypeFqn);
|
||||
|
||||
List<String> expanded = new ArrayList<>();
|
||||
for (String pe : polymorphicEvents) {
|
||||
if (pe == null) {
|
||||
@@ -118,6 +128,10 @@ public final class MachineEnumCanonicalizer {
|
||||
if (!enumTypesMatch(machineEventTypeFqn, symbolicType)) {
|
||||
continue;
|
||||
}
|
||||
if (!transitionEvents.isEmpty()) {
|
||||
expanded.addAll(transitionEvents);
|
||||
continue;
|
||||
}
|
||||
if (context != null) {
|
||||
List<String> enumValues = context.getEnumValues(stripGenerics(machineEventTypeFqn));
|
||||
if (enumValues != null) {
|
||||
@@ -202,7 +216,7 @@ public final class MachineEnumCanonicalizer {
|
||||
List<Transition> machineTransitions,
|
||||
boolean skipCanonicalization) {
|
||||
TriggerPoint expanded = skipCanonicalization
|
||||
? expandSymbolicOnly(trigger, machineTypes, context)
|
||||
? expandSymbolicOnly(trigger, machineTypes, context, machineTransitions)
|
||||
: canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context);
|
||||
if (expanded == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
|
||||
return expanded;
|
||||
@@ -210,7 +224,7 @@ public final class MachineEnumCanonicalizer {
|
||||
List<String> postExpand = expanded.getPolymorphicEvents();
|
||||
if (postExpand != null && postExpand.stream().anyMatch(pe -> pe != null && pe.startsWith("<SYMBOLIC:"))) {
|
||||
postExpand = expandSymbolicPolymorphicEvents(
|
||||
postExpand, machineTypes.eventTypeFqn(), context);
|
||||
postExpand, machineTypes.eventTypeFqn(), context, machineTransitions);
|
||||
postExpand = narrowExpandedPolymorphicEvents(
|
||||
postExpand,
|
||||
expanded.getConstraint(),
|
||||
@@ -221,7 +235,8 @@ public final class MachineEnumCanonicalizer {
|
||||
expanded = expanded.toBuilder().polymorphicEvents(postExpand).build();
|
||||
}
|
||||
}
|
||||
if (hasConcretePolymorphicEvents(expanded.getPolymorphicEvents())) {
|
||||
if (hasConcretePolymorphicEvents(expanded.getPolymorphicEvents())
|
||||
|| (expanded.getPolymorphicEvents() != null && !expanded.getPolymorphicEvents().isEmpty())) {
|
||||
List<String> narrowed = narrowPolymorphicCandidates(
|
||||
expanded.getPolymorphicEvents(),
|
||||
expanded.getConstraint(),
|
||||
@@ -255,12 +270,13 @@ public final class MachineEnumCanonicalizer {
|
||||
private static TriggerPoint expandSymbolicOnly(
|
||||
TriggerPoint trigger,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||
CodebaseContext context) {
|
||||
CodebaseContext context,
|
||||
List<Transition> machineTransitions) {
|
||||
if (trigger == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
|
||||
return trigger;
|
||||
}
|
||||
List<String> expanded = expandSymbolicPolymorphicEvents(
|
||||
trigger.getPolymorphicEvents(), machineTypes.eventTypeFqn(), context);
|
||||
trigger.getPolymorphicEvents(), machineTypes.eventTypeFqn(), context, machineTransitions);
|
||||
if (java.util.Objects.equals(expanded, trigger.getPolymorphicEvents())) {
|
||||
return trigger;
|
||||
}
|
||||
@@ -315,21 +331,21 @@ public final class MachineEnumCanonicalizer {
|
||||
List<Transition> machineTransitions,
|
||||
CodebaseContext context) {
|
||||
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn);
|
||||
if (!transitionEvents.isEmpty()) {
|
||||
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||
transitionEvents, constraint, eventTypeFqn, context);
|
||||
return filtered.isEmpty() ? transitionEvents : filtered;
|
||||
if (transitionEvents.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> enumConstants = allPackageCanonicalEnumConstants(eventTypeFqn, context);
|
||||
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||
enumConstants, constraint, eventTypeFqn, context);
|
||||
transitionEvents, constraint, eventTypeFqn, context);
|
||||
if (!filtered.isEmpty()) {
|
||||
return filtered;
|
||||
}
|
||||
if (!transitionEvents.isEmpty()) {
|
||||
if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(constraint)) {
|
||||
if (EnumMemberPredicateEvaluator.hasResolvablePredicateMethods(constraint, eventTypeFqn, context)) {
|
||||
return List.of();
|
||||
}
|
||||
return transitionEvents;
|
||||
}
|
||||
return List.of();
|
||||
return transitionEvents;
|
||||
}
|
||||
|
||||
private static List<String> narrowPolymorphicCandidates(
|
||||
@@ -339,56 +355,65 @@ public final class MachineEnumCanonicalizer {
|
||||
List<Transition> machineTransitions,
|
||||
CodebaseContext context) {
|
||||
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn);
|
||||
List<String> result = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||
current, constraint, eventTypeFqn, context);
|
||||
if (result.isEmpty() && !transitionEvents.isEmpty()) {
|
||||
result = new ArrayList<>(transitionEvents);
|
||||
}
|
||||
if (!looksOverBroad(result, transitionEvents, constraint)) {
|
||||
return result;
|
||||
}
|
||||
if (!transitionEvents.isEmpty()) {
|
||||
List<String> intersected = new ArrayList<>();
|
||||
for (String event : result) {
|
||||
if (transitionEvents.contains(event)) {
|
||||
intersected.add(event);
|
||||
List<String> result = current == null ? List.of() : new ArrayList<>(current);
|
||||
|
||||
if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(constraint)) {
|
||||
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||
result, constraint, eventTypeFqn, context);
|
||||
if (!filtered.isEmpty()) {
|
||||
result = filtered;
|
||||
} else if (!transitionEvents.isEmpty()) {
|
||||
filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||
transitionEvents, constraint, eventTypeFqn, context);
|
||||
if (!filtered.isEmpty()) {
|
||||
result = filtered;
|
||||
} else if (EnumMemberPredicateEvaluator.hasResolvablePredicateMethods(
|
||||
constraint, eventTypeFqn, context)) {
|
||||
result = List.of();
|
||||
} else {
|
||||
result = transitionEvents;
|
||||
}
|
||||
} else {
|
||||
result = List.of();
|
||||
}
|
||||
if (!intersected.isEmpty()) {
|
||||
return intersected;
|
||||
}
|
||||
List<String> transitionFiltered = EnumMemberPredicateEvaluator.filterEnumConstants(
|
||||
transitionEvents, constraint, eventTypeFqn, context);
|
||||
return transitionFiltered.isEmpty() ? transitionEvents : transitionFiltered;
|
||||
}
|
||||
|
||||
if (!transitionEvents.isEmpty()) {
|
||||
result = capToConfiguredTransitionEvents(result, transitionEvents);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean looksOverBroad(
|
||||
List<String> current,
|
||||
List<String> transitionEvents,
|
||||
String constraint) {
|
||||
if (current == null || current.isEmpty()) {
|
||||
return false;
|
||||
/**
|
||||
* Fail-closed ceiling: polymorphic candidates must never exceed events configured on the machine.
|
||||
*/
|
||||
static List<String> capToConfiguredTransitionEvents(
|
||||
List<String> candidates,
|
||||
List<String> transitionEvents) {
|
||||
if (transitionEvents == null || transitionEvents.isEmpty()) {
|
||||
return candidates == null ? List.of() : candidates;
|
||||
}
|
||||
if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(constraint)) {
|
||||
return true;
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
if (current.size() <= 1) {
|
||||
return false;
|
||||
}
|
||||
if (transitionEvents.isEmpty()) {
|
||||
return current.size() > 1;
|
||||
}
|
||||
if (current.size() > transitionEvents.size()) {
|
||||
return true;
|
||||
}
|
||||
for (String event : current) {
|
||||
if (!transitionEvents.contains(event)) {
|
||||
return true;
|
||||
List<String> capped = new ArrayList<>();
|
||||
for (String candidate : candidates) {
|
||||
if (candidate == null) {
|
||||
continue;
|
||||
}
|
||||
if (transitionEvents.contains(candidate)) {
|
||||
capped.add(candidate);
|
||||
continue;
|
||||
}
|
||||
String constant = constantName(candidate);
|
||||
for (String transitionEvent : transitionEvents) {
|
||||
if (constantName(transitionEvent).equals(constant)) {
|
||||
capped.add(transitionEvent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return capped;
|
||||
}
|
||||
|
||||
public static List<String> polymorphicEventsFromTransitions(
|
||||
@@ -423,23 +448,6 @@ public final class MachineEnumCanonicalizer {
|
||||
.anyMatch(pe -> classifyTriggerEvent(pe) == TriggerEventKind.CANONICAL_ENUM);
|
||||
}
|
||||
|
||||
private static List<String> allPackageCanonicalEnumConstants(String eventTypeFqn, CodebaseContext context) {
|
||||
if (eventTypeFqn == null || eventTypeFqn.isBlank() || context == null) {
|
||||
return List.of();
|
||||
}
|
||||
String enumType = stripGenerics(eventTypeFqn);
|
||||
List<String> enumValues = context.getEnumValues(enumType);
|
||||
if (enumValues == null || enumValues.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> canonical = new ArrayList<>();
|
||||
for (String value : enumValues) {
|
||||
String constant = value.contains(".") ? value.substring(value.lastIndexOf('.') + 1) : value;
|
||||
canonical.add(enumType + "." + constant);
|
||||
}
|
||||
return canonical;
|
||||
}
|
||||
|
||||
public static String qualifyEventIdentifier(String event, String eventTypeFqn) {
|
||||
if (event == null || eventTypeFqn == null || event.isEmpty()) {
|
||||
return event;
|
||||
|
||||
@@ -971,14 +971,6 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
polymorphicEvents.add(methodReturn);
|
||||
}
|
||||
}
|
||||
if (!hasConcreteEnumConstants(polymorphicEvents, declaredType, context)
|
||||
&& polymorphicEvents.stream().anyMatch(pe -> pe != null && pe.startsWith("<SYMBOLIC:"))
|
||||
&& declaredType != null) {
|
||||
List<String> expandedEnumValues = expandDeclaredEnumValues(declaredType);
|
||||
if (!expandedEnumValues.isEmpty()) {
|
||||
polymorphicEvents = new ArrayList<>(expandedEnumValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2403,26 +2395,16 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
if (tdOuter != null) {
|
||||
String callerFqn = context.getFqn(tdOuter) + "." + md.getName().getIdentifier();
|
||||
String methodName = node.getName().getIdentifier();
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
String superFqn = context.getSuperclassFqn(td);
|
||||
if (superFqn != null) {
|
||||
TypeDeclaration superTd = context.getTypeDeclaration(superFqn);
|
||||
String calledMethod = null;
|
||||
if (superTd != null) {
|
||||
calledMethod = resolveMethodInType(superTd, methodName);
|
||||
}
|
||||
if (calledMethod == null) {
|
||||
calledMethod = superFqn + "." + methodName;
|
||||
}
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
String receiver = "super";
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils
|
||||
.findConditionConstraint(node);
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
String calledMethod = InheritanceCallTargetResolver.resolveSuperMethod(
|
||||
context, tdOuter, methodName);
|
||||
if (calledMethod != null) {
|
||||
List<String> args = resolveArguments(node.arguments());
|
||||
String receiver = "super";
|
||||
String constraint = click.kamil.springstatemachineexporter.ast.common.AstUtils
|
||||
.findConditionConstraint(node);
|
||||
CallEdge edge = new CallEdge(calledMethod, args, receiver);
|
||||
edge.setConstraint(constraint);
|
||||
graph.computeIfAbsent(callerFqn, k -> new ArrayList<>()).add(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,14 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
}
|
||||
|
||||
TypeDeclaration enclosingType = findEnclosingType(node);
|
||||
|
||||
if (receiver instanceof SuperMethodInvocation) {
|
||||
return InheritanceCallTargetResolver.resolveSuperMethod(context, enclosingType, methodName);
|
||||
}
|
||||
|
||||
if (receiver == null) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
return resolveMethodInTypeHierarchy(td, methodName);
|
||||
}
|
||||
return null;
|
||||
return InheritanceCallTargetResolver.resolveInstanceMethod(context, enclosingType, methodName);
|
||||
}
|
||||
|
||||
ITypeBinding binding = receiver.resolveTypeBinding();
|
||||
@@ -42,10 +44,7 @@ public class HeuristicCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
|
||||
if (receiver instanceof ThisExpression) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
return context.getFqn(td) + "." + methodName;
|
||||
}
|
||||
return InheritanceCallTargetResolver.resolveInstanceMethod(context, enclosingType, methodName);
|
||||
}
|
||||
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.ASTNode;
|
||||
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
/**
|
||||
* Resolves call-graph target FQNs for {@code this}, implicit, and {@code super} dispatch hops.
|
||||
* Uses the declaring type of the resolved method (parent class for inherited members).
|
||||
*/
|
||||
final class InheritanceCallTargetResolver {
|
||||
|
||||
private InheritanceCallTargetResolver() {
|
||||
}
|
||||
|
||||
static String resolveInstanceMethod(
|
||||
CodebaseContext context,
|
||||
TypeDeclaration enclosingType,
|
||||
String methodName) {
|
||||
if (context == null || enclosingType == null || methodName == null || methodName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
MethodDeclaration method = context.findMethodDeclaration(enclosingType, methodName, true);
|
||||
if (method == null) {
|
||||
return null;
|
||||
}
|
||||
TypeDeclaration declaringType = findEnclosingTypeDeclaration(method);
|
||||
if (declaringType == null) {
|
||||
return null;
|
||||
}
|
||||
String fqn = context.getFqn(declaringType);
|
||||
return fqn == null ? null : fqn + "." + methodName;
|
||||
}
|
||||
|
||||
static String resolveSuperMethod(
|
||||
CodebaseContext context,
|
||||
TypeDeclaration enclosingType,
|
||||
String methodName) {
|
||||
if (context == null || enclosingType == null || methodName == null || methodName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String superFqn = context.getSuperclassFqn(enclosingType);
|
||||
if (superFqn == null) {
|
||||
return null;
|
||||
}
|
||||
TypeDeclaration superType = context.getTypeDeclaration(superFqn);
|
||||
if (superType == null) {
|
||||
return superFqn + "." + methodName;
|
||||
}
|
||||
String resolved = resolveInstanceMethod(context, superType, methodName);
|
||||
return resolved != null ? resolved : superFqn + "." + methodName;
|
||||
}
|
||||
|
||||
private static TypeDeclaration findEnclosingTypeDeclaration(ASTNode node) {
|
||||
ASTNode current = node;
|
||||
while (current != null) {
|
||||
if (current instanceof TypeDeclaration typeDeclaration) {
|
||||
return typeDeclaration;
|
||||
}
|
||||
current = current.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -56,12 +56,14 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
}
|
||||
|
||||
TypeDeclaration enclosingType = findEnclosingType(node);
|
||||
|
||||
if (receiver instanceof SuperMethodInvocation) {
|
||||
return InheritanceCallTargetResolver.resolveSuperMethod(context, enclosingType, methodName);
|
||||
}
|
||||
|
||||
if (receiver == null) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
return resolveMethodInType(td, methodName);
|
||||
}
|
||||
return null;
|
||||
return InheritanceCallTargetResolver.resolveInstanceMethod(context, enclosingType, methodName);
|
||||
}
|
||||
|
||||
if (injectionAnalyzer != null) {
|
||||
@@ -122,10 +124,7 @@ public class JdtCallGraphEngine extends AbstractCallGraphEngine {
|
||||
}
|
||||
|
||||
if (receiver instanceof ThisExpression) {
|
||||
TypeDeclaration td = findEnclosingType(node);
|
||||
if (td != null) {
|
||||
return context.getFqn(td) + "." + methodName;
|
||||
}
|
||||
return InheritanceCallTargetResolver.resolveInstanceMethod(context, enclosingType, methodName);
|
||||
}
|
||||
|
||||
if (receiver instanceof SimpleName sn) {
|
||||
|
||||
Reference in New Issue
Block a user