Harden polymorphic event narrowing to prevent over-linking transitions.

Fail closed when enum predicates or polymorphicEvents cannot be resolved, infer candidates from configured transitions, and validate exports that link more transitions than the machine defines.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 16:22:58 +02:00
parent 77bf840465
commit 83008a6898
6 changed files with 417 additions and 51 deletions

View File

@@ -62,16 +62,13 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
}
if (polyEvents.isEmpty() && isDynamicVariable(rawTriggerEvent)) {
if (triggerPoint.getEventTypeFqn() != null) {
if (smEventRaw.startsWith(triggerPoint.getEventTypeFqn() + ".")) {
return true;
}
if (isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn()) && !smEventRaw.contains(".")) {
return true;
}
if (triggerPoint.getEventTypeFqn() != null && !isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) {
return false;
}
return true;
if (triggerPoint.getEventTypeFqn() != null && isStringTypeOrPrimitive(triggerPoint.getEventTypeFqn())) {
return !smEventRaw.contains(".");
}
return !smEventRaw.contains(".");
}
return matchEventNames(rawTriggerEvent, smEventRaw, triggerPoint.getEventTypeFqn());

View File

@@ -6,12 +6,15 @@ import org.eclipse.jdt.core.dom.BooleanLiteral;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.TypeDeclaration;
@@ -76,7 +79,7 @@ public final class EnumMemberPredicateEvaluator {
}
List<String> filtered = new ArrayList<>();
for (String candidate : candidates) {
if (satisfiesPredicates(candidate, predicates, enumDecl)) {
if (satisfiesPredicates(candidate, predicates, enumDecl, context)) {
filtered.add(candidate);
}
}
@@ -86,16 +89,17 @@ public final class EnumMemberPredicateEvaluator {
private static boolean satisfiesPredicates(
String canonicalConstantFqn,
List<PredicateCall> predicates,
EnumDeclaration enumDecl) {
EnumDeclaration enumDecl,
CodebaseContext context) {
String constantName = constantSimpleName(canonicalConstantFqn);
if (constantName == null) {
return false;
}
Map<String, Boolean> boolFields = resolveConstantBooleanFields(enumDecl, constantName);
for (PredicateCall predicate : predicates) {
Boolean methodResult = evaluateBooleanMethod(enumDecl, predicate.methodName(), boolFields);
Boolean methodResult = evaluateBooleanMethod(enumDecl, predicate.methodName(), boolFields, context);
if (methodResult == null) {
return true;
return false;
}
boolean expected = predicate.negated ? !methodResult : methodResult;
if (!expected) {
@@ -108,8 +112,12 @@ public final class EnumMemberPredicateEvaluator {
static Boolean evaluateBooleanMethod(
EnumDeclaration enumDecl,
String methodName,
Map<String, Boolean> boolFields) {
Map<String, Boolean> boolFields,
CodebaseContext context) {
MethodDeclaration method = findParameterlessMethod(enumDecl, methodName);
if (method == null && context != null) {
method = findInterfaceParameterlessMethod(enumDecl, methodName, context);
}
if (method == null || method.getBody() == null) {
return null;
}
@@ -118,7 +126,7 @@ public final class EnumMemberPredicateEvaluator {
@Override
public boolean visit(ReturnStatement node) {
if (result[0] == null) {
result[0] = evaluateBooleanExpression(node.getExpression(), boolFields);
result[0] = evaluateBooleanExpression(node.getExpression(), boolFields, enumDecl, context);
}
return super.visit(node);
}
@@ -179,7 +187,11 @@ public final class EnumMemberPredicateEvaluator {
}
}
private static Boolean evaluateBooleanExpression(Expression expr, Map<String, Boolean> boolFields) {
private static Boolean evaluateBooleanExpression(
Expression expr,
Map<String, Boolean> boolFields,
EnumDeclaration enumDecl,
CodebaseContext context) {
if (expr instanceof BooleanLiteral bl) {
return bl.booleanValue();
}
@@ -191,6 +203,12 @@ public final class EnumMemberPredicateEvaluator {
return boolFields.get(fa.getName().getIdentifier());
}
}
if (expr instanceof MethodInvocation mi
&& mi.arguments().isEmpty()
&& enumDecl != null
&& context != null) {
return evaluateBooleanMethod(enumDecl, mi.getName().getIdentifier(), boolFields, context);
}
return null;
}
@@ -198,8 +216,11 @@ public final class EnumMemberPredicateEvaluator {
return expr instanceof BooleanLiteral bl ? bl.booleanValue() : null;
}
private static MethodDeclaration findParameterlessMethod(EnumDeclaration enumDecl, String methodName) {
for (Object bodyObj : enumDecl.bodyDeclarations()) {
private static MethodDeclaration findParameterlessMethod(AbstractTypeDeclaration typeDecl, String methodName) {
if (typeDecl == null) {
return null;
}
for (Object bodyObj : typeDecl.bodyDeclarations()) {
if (bodyObj instanceof MethodDeclaration md
&& !md.isConstructor()
&& methodName.equals(md.getName().getIdentifier())
@@ -210,6 +231,25 @@ public final class EnumMemberPredicateEvaluator {
return null;
}
private static MethodDeclaration findInterfaceParameterlessMethod(
EnumDeclaration enumDecl,
String methodName,
CodebaseContext context) {
CompilationUnit cu = enumDecl.getRoot() instanceof CompilationUnit root ? root : null;
for (Object typeObj : enumDecl.superInterfaceTypes()) {
if (!(typeObj instanceof SimpleType simpleType)) {
continue;
}
String interfaceName = simpleType.getName().getFullyQualifiedName();
TypeDeclaration interfaceDecl = context.getTypeDeclaration(interfaceName, cu);
MethodDeclaration method = findParameterlessMethod(interfaceDecl, methodName);
if (method != null) {
return method;
}
}
return null;
}
private static MethodDeclaration findEnumConstructor(EnumDeclaration enumDecl) {
MethodDeclaration implicit = null;
for (Object bodyObj : enumDecl.bodyDeclarations()) {

View File

@@ -179,7 +179,7 @@ public final class MachineEnumCanonicalizer {
List<Transition> machineTransitions,
boolean skipCanonicalization) {
TriggerPoint expanded = skipCanonicalization
? trigger
? expandSymbolicOnly(trigger, machineTypes, context)
: canonicalizeAndExpandTriggerPoint(trigger, machineTypes, context);
if (expanded == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
return expanded;
@@ -199,21 +199,77 @@ public final class MachineEnumCanonicalizer {
}
return expanded;
}
if (classifyTriggerEvent(expanded.getEvent()) != TriggerEventKind.DYNAMIC_EXPRESSION) {
return expanded;
if (shouldInferPolymorphicEvents(expanded, machineTransitions)) {
List<String> machineEvents = inferPolymorphicCandidates(
expanded.getConstraint(),
machineTypes.eventTypeFqn(),
machineTransitions,
context);
if (!machineEvents.isEmpty()) {
return expanded.toBuilder()
.polymorphicEvents(machineEvents)
.ambiguous(machineEvents.size() > 1)
.build();
}
}
List<String> machineEvents = inferPolymorphicCandidates(
expanded.getConstraint(),
machineTypes.eventTypeFqn(),
machineTransitions,
context);
if (machineEvents.isEmpty()) {
return expanded;
return expanded;
}
private static TriggerPoint expandSymbolicOnly(
TriggerPoint trigger,
StateMachineTypeResolver.MachineTypes machineTypes,
CodebaseContext context) {
if (trigger == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
return trigger;
}
return expanded.toBuilder()
.polymorphicEvents(machineEvents)
.ambiguous(machineEvents.size() > 1)
.build();
List<String> expanded = expandSymbolicPolymorphicEvents(
trigger.getPolymorphicEvents(), machineTypes.eventTypeFqn(), context);
if (java.util.Objects.equals(expanded, trigger.getPolymorphicEvents())) {
return trigger;
}
return trigger.toBuilder().polymorphicEvents(expanded).build();
}
private static boolean shouldInferPolymorphicEvents(
TriggerPoint trigger,
List<Transition> machineTransitions) {
if (trigger == null || machineTransitions == null || machineTransitions.isEmpty()) {
return false;
}
if (hasOnlySymbolicPolymorphicEvents(trigger.getPolymorphicEvents())) {
return true;
}
List<String> polyEvents = trigger.getPolymorphicEvents();
if (polyEvents != null && !polyEvents.isEmpty()) {
return false;
}
if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(trigger.getConstraint())) {
return true;
}
if (trigger.isExternal() || trigger.isAmbiguous()) {
return true;
}
return classifyTriggerEvent(trigger.getEvent()) == TriggerEventKind.DYNAMIC_EXPRESSION;
}
private static boolean hasOnlySymbolicPolymorphicEvents(List<String> polymorphicEvents) {
if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
return false;
}
boolean hasSymbolic = false;
for (String pe : polymorphicEvents) {
if (pe == null) {
continue;
}
if (pe.startsWith("<SYMBOLIC:")) {
hasSymbolic = true;
continue;
}
if (classifyTriggerEvent(pe) == TriggerEventKind.CANONICAL_ENUM) {
return false;
}
}
return hasSymbolic;
}
private static List<String> inferPolymorphicCandidates(
@@ -233,6 +289,9 @@ public final class MachineEnumCanonicalizer {
if (!filtered.isEmpty()) {
return filtered;
}
if (!transitionEvents.isEmpty()) {
return transitionEvents;
}
return List.of();
}
@@ -242,9 +301,12 @@ public final class MachineEnumCanonicalizer {
String eventTypeFqn,
List<Transition> machineTransitions,
CodebaseContext context) {
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn);
List<String> result = EnumMemberPredicateEvaluator.filterEnumConstants(
current, constraint, eventTypeFqn, context);
List<String> transitionEvents = polymorphicEventsFromTransitions(machineTransitions, eventTypeFqn);
if (result.isEmpty() && !transitionEvents.isEmpty()) {
result = new ArrayList<>(transitionEvents);
}
if (!looksOverBroad(result, transitionEvents, constraint)) {
return result;
}
@@ -281,10 +343,18 @@ public final class MachineEnumCanonicalizer {
if (transitionEvents.isEmpty()) {
return current.size() > 1;
}
return current.size() > transitionEvents.size();
if (current.size() > transitionEvents.size()) {
return true;
}
for (String event : current) {
if (!transitionEvents.contains(event)) {
return true;
}
}
return false;
}
static List<String> polymorphicEventsFromTransitions(
public static List<String> polymorphicEventsFromTransitions(
List<Transition> machineTransitions,
String eventTypeFqn) {
if (machineTransitions == null || machineTransitions.isEmpty() || eventTypeFqn == null) {

View File

@@ -266,6 +266,8 @@ public final class AnalysisCanonicalFormValidator {
}
validateMatchedTransitionsWhenResolvable(
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations);
validateOverLinkedPolymorphicEvents(
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations);
}
if (chain.getMatchedTransitions() != null) {
for (int j = 0; j < chain.getMatchedTransitions().size(); j++) {
@@ -339,6 +341,74 @@ public final class AnalysisCanonicalFormValidator {
}
}
private static void validateOverLinkedPolymorphicEvents(
String chainPrefix,
CallChain chain,
TriggerPoint trigger,
List<Transition> transitions,
String eventTypeFqn,
List<Violation> violations) {
if (trigger.getPolymorphicEvents() == null || trigger.getPolymorphicEvents().isEmpty()) {
return;
}
if (!MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(trigger.getPolymorphicEvents())) {
return;
}
List<String> transitionEvents =
MachineEnumCanonicalizer.polymorphicEventsFromTransitions(transitions, eventTypeFqn);
if (transitionEvents.isEmpty()) {
return;
}
List<String> extras = new ArrayList<>();
for (String polyEvent : trigger.getPolymorphicEvents()) {
if (!transitionEvents.contains(polyEvent)) {
extras.add(polyEvent);
}
}
if (!extras.isEmpty()) {
violations.add(new Violation(
chainPrefix + ".triggerPoint.polymorphicEvents",
String.valueOf(trigger.getPolymorphicEvents().size()),
"subset of configured transition events; unexpected: " + extras));
return;
}
int matchingTransitionCount = countMatchingConfiguredTransitions(
transitions, trigger.getPolymorphicEvents(), eventTypeFqn);
if (matchingTransitionCount > 0
&& chain.getMatchedTransitions() != null
&& chain.getMatchedTransitions().size() > matchingTransitionCount) {
violations.add(new Violation(
chainPrefix + ".matchedTransitions",
String.valueOf(chain.getMatchedTransitions().size()),
"at most " + matchingTransitionCount + " for configured transitions"));
}
}
private static int countMatchingConfiguredTransitions(
List<Transition> transitions,
List<String> polymorphicEvents,
String eventTypeFqn) {
if (transitions == null || transitions.isEmpty() || polymorphicEvents == null) {
return 0;
}
int count = 0;
for (Transition transition : transitions) {
if (transition.getEvent() == null) {
continue;
}
String smEvent = transition.getEvent().fullIdentifier() != null
? transition.getEvent().fullIdentifier()
: transition.getEvent().rawName();
for (String polyEvent : polymorphicEvents) {
if (eventsMatch(polyEvent, smEvent, eventTypeFqn)) {
count++;
break;
}
}
}
return count;
}
private static void validateMatchedTransitionsWhenResolvable(
String chainPrefix,
CallChain chain,