From 828f1260cdb862e15f3ca538ddb8d0c0ffe36d92 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 14 Jul 2026 05:43:12 +0200 Subject: [PATCH] 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 --- .../resolver/MachineEnumCanonicalizer.java | 30 +++++++++++++++-- .../MachineEnumCanonicalizerTest.java | 32 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java index ac5c887..a66a435 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizer.java @@ -258,7 +258,7 @@ public final class MachineEnumCanonicalizer { } return expanded; } - if (shouldInferPolymorphicEvents(expanded, machineTransitions)) { + if (shouldInferPolymorphicEvents(expanded, machineTransitions, context)) { List machineEvents = inferPolymorphicCandidates( expanded.getConstraint(), machineTypes.eventTypeFqn(), @@ -292,11 +292,15 @@ public final class MachineEnumCanonicalizer { private static boolean shouldInferPolymorphicEvents( TriggerPoint trigger, - List machineTransitions) { + List machineTransitions, + CodebaseContext context) { if (trigger == null || machineTransitions == null || machineTransitions.isEmpty()) { return false; } if (hasOnlySymbolicPolymorphicEvents(trigger.getPolymorphicEvents())) { + if (context != null && hasAmbiguousSymbolicPolymorphicType(trigger.getPolymorphicEvents(), context)) { + return false; + } return true; } List polyEvents = trigger.getPolymorphicEvents(); @@ -306,12 +310,32 @@ public final class MachineEnumCanonicalizer { if (EnumMemberPredicateEvaluator.hasEnumMemberPredicates(trigger.getConstraint())) { return true; } - if (trigger.isExternal() || trigger.isAmbiguous()) { + if (trigger.isExternal()) { return true; } + if (trigger.isAmbiguous()) { + return false; + } return classifyTriggerEvent(trigger.getEvent()) == TriggerEventKind.DYNAMIC_EXPRESSION; } + private static boolean hasAmbiguousSymbolicPolymorphicType( + List polymorphicEvents, + CodebaseContext context) { + if (polymorphicEvents == null) { + return false; + } + for (String pe : polymorphicEvents) { + if (pe != null && pe.startsWith("")) { + String symbolicType = pe.substring(" polymorphicEvents) { if (polymorphicEvents == null || polymorphicEvents.isEmpty()) { return false; diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerTest.java index 1c884ff..794ace5 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/resolver/MachineEnumCanonicalizerTest.java @@ -231,6 +231,38 @@ class MachineEnumCanonicalizerTest { 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 void shouldValidateRawNameConsistencyForCanonicalHelpers() { assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(