Stop inferring machine transitions onto ambiguous triggers

Do not backfill polymorphic events from configured transitions when a
trigger is ambiguous without enum-member constraints, and block symbolic
inference when the symbolic type simple name is ambiguous across packages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 05:43:12 +02:00
parent fb3218e1a2
commit 828f1260cd
2 changed files with 59 additions and 3 deletions

View File

@@ -258,7 +258,7 @@ public final class MachineEnumCanonicalizer {
} }
return expanded; return expanded;
} }
if (shouldInferPolymorphicEvents(expanded, machineTransitions)) { if (shouldInferPolymorphicEvents(expanded, machineTransitions, context)) {
List<String> machineEvents = inferPolymorphicCandidates( List<String> machineEvents = inferPolymorphicCandidates(
expanded.getConstraint(), expanded.getConstraint(),
machineTypes.eventTypeFqn(), machineTypes.eventTypeFqn(),
@@ -292,11 +292,15 @@ public final class MachineEnumCanonicalizer {
private static boolean shouldInferPolymorphicEvents( private static boolean shouldInferPolymorphicEvents(
TriggerPoint trigger, TriggerPoint trigger,
List<Transition> machineTransitions) { List<Transition> machineTransitions,
CodebaseContext context) {
if (trigger == null || machineTransitions == null || machineTransitions.isEmpty()) { if (trigger == null || machineTransitions == null || machineTransitions.isEmpty()) {
return false; return false;
} }
if (hasOnlySymbolicPolymorphicEvents(trigger.getPolymorphicEvents())) { if (hasOnlySymbolicPolymorphicEvents(trigger.getPolymorphicEvents())) {
if (context != null && hasAmbiguousSymbolicPolymorphicType(trigger.getPolymorphicEvents(), context)) {
return false;
}
return true; return true;
} }
List<String> polyEvents = trigger.getPolymorphicEvents(); List<String> polyEvents = trigger.getPolymorphicEvents();
@@ -306,12 +310,32 @@ public final class MachineEnumCanonicalizer {
if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(trigger.getConstraint())) { if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(trigger.getConstraint())) {
return true; return true;
} }
if (trigger.isExternal() || trigger.isAmbiguous()) { if (trigger.isExternal()) {
return true; return true;
} }
if (trigger.isAmbiguous()) {
return false;
}
return classifyTriggerEvent(trigger.getEvent()) == TriggerEventKind.DYNAMIC_EXPRESSION; return classifyTriggerEvent(trigger.getEvent()) == TriggerEventKind.DYNAMIC_EXPRESSION;
} }
private static boolean hasAmbiguousSymbolicPolymorphicType(
List<String> polymorphicEvents,
CodebaseContext context) {
if (polymorphicEvents == null) {
return false;
}
for (String pe : polymorphicEvents) {
if (pe != null && pe.startsWith("<SYMBOLIC: ") && pe.endsWith(".*>")) {
String symbolicType = pe.substring("<SYMBOLIC: ".length(), pe.length() - 3).trim();
if (context.isAmbiguousSimpleName(symbolicType)) {
return true;
}
}
}
return false;
}
private static boolean hasOnlySymbolicPolymorphicEvents(List<String> polymorphicEvents) { private static boolean hasOnlySymbolicPolymorphicEvents(List<String> polymorphicEvents) {
if (polymorphicEvents == null || polymorphicEvents.isEmpty()) { if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
return false; return false;

View File

@@ -231,6 +231,38 @@ class MachineEnumCanonicalizerTest {
assertThat(expanded).isEmpty(); assertThat(expanded).isEmpty();
} }
@Test
void shouldNotInferMachineTransitionsForAmbiguousTriggerWithoutConstraint(@TempDir Path tempDir)
throws IOException {
Files.createDirectories(tempDir.resolve("a"));
Files.createDirectories(tempDir.resolve("b"));
Files.writeString(tempDir.resolve("a/OrderEvent.java"),
"package a; public enum OrderEvent { PAY, SHIP }");
Files.writeString(tempDir.resolve("b/OrderEvent.java"),
"package b; public enum OrderEvent { CANCEL }");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TriggerPoint trigger = TriggerPoint.builder()
.event("OrderEvent.valueOf(eventStr)")
.ambiguous(true)
.polymorphicEvents(List.of())
.build();
StateMachineTypeResolver.MachineTypes types =
new StateMachineTypeResolver.MachineTypes(null, "a.OrderEvent");
Transition pay = new Transition();
pay.setEvent(Event.of("PAY", "a.OrderEvent.PAY"));
pay.setSourceStates(List.of(State.of("NEW", "NEW")));
pay.setTargetStates(List.of(State.of("PAID", "PAID")));
TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
trigger, types, context, List.of(pay), true);
assertThat(enriched.getPolymorphicEvents()).isNullOrEmpty();
}
@Test @Test
void shouldValidateRawNameConsistencyForCanonicalHelpers() { void shouldValidateRawNameConsistencyForCanonicalHelpers() {
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier( assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(