Restore machine-scoped enum linking when global simple names are ambiguous.

Call-graph ambiguous metadata must not blank matchedTransitions; trust import-style polymorphic widen when the machine event type is known.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 19:34:08 +02:00
parent 5b0778301a
commit 6ca8e64750
13 changed files with 309 additions and 31 deletions

View File

@@ -49,7 +49,8 @@ public final class CallChainLinkPolicy {
&& !isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn, context); && !isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn, context);
} }
return MachineEnumCanonicalizer.isDynamicTriggerExpression(event) return MachineEnumCanonicalizer.isDynamicTriggerExpression(event)
&& event != null; && event != null
&& !isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn, context);
} }
/** /**
@@ -87,9 +88,7 @@ public final class CallChainLinkPolicy {
if (pe == null || pe.startsWith("<SYMBOLIC:") || pe.startsWith("ENUM_SET:") || !pe.contains(".")) { if (pe == null || pe.startsWith("<SYMBOLIC:") || pe.startsWith("ENUM_SET:") || !pe.contains(".")) {
return false; return false;
} }
String enumType = pe.substring(0, pe.lastIndexOf('.')); if (!MachineEnumCanonicalizer.polymorphicEventMatchesMachineEventType(pe, eventTypeFqn, context)) {
if (!enumType.contains(".")
|| !MachineEnumCanonicalizer.enumTypesMatch(eventTypeFqn, enumType, context)) {
return false; return false;
} }
} }
@@ -121,7 +120,6 @@ public final class CallChainLinkPolicy {
return LinkResolution.UNRESOLVED_EXTERNAL; return LinkResolution.UNRESOLVED_EXTERNAL;
} }
if (ambiguousSource if (ambiguousSource
|| (trigger != null && trigger.isAmbiguous())
|| shouldFailClosedOnAmbiguousCallGraphWiden(trigger, machineEventTypeFqn, context)) { || shouldFailClosedOnAmbiguousCallGraphWiden(trigger, machineEventTypeFqn, context)) {
return LinkResolution.AMBIGUOUS_WIDEN; return LinkResolution.AMBIGUOUS_WIDEN;
} }

View File

@@ -5,7 +5,9 @@ import click.kamil.springstatemachineexporter.analysis.enricher.routing.Heuristi
import click.kamil.springstatemachineexporter.analysis.model.CallChain; import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.Event;
import click.kamil.springstatemachineexporter.model.Transition; import click.kamil.springstatemachineexporter.model.Transition;
import java.util.ArrayList; import java.util.ArrayList;
@@ -57,14 +59,48 @@ public final class MachineScopeFilter {
return chains == null ? List.of() : chains; return chains == null ? List.of() : chains;
} }
List<CallChain> filtered = new ArrayList<>(); List<CallChain> filtered = new ArrayList<>();
String machineEventTypeFqn = resolveMachineEventTypeFqn(machineName, context, machineTransitions);
for (CallChain chain : chains) { for (CallChain chain : chains) {
if (ROUTING.hasProvenMachineAffinity(chain, machineName, context, machineTransitions)) { if (ROUTING.hasProvenMachineAffinity(chain, machineName, context, machineTransitions)) {
filtered.add(chain); filtered.add(chain);
continue;
}
TriggerPoint trigger = chain.getTriggerPoint();
if (trigger != null
&& machineEventTypeFqn != null
&& CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(trigger, machineEventTypeFqn, context)) {
filtered.add(chain);
} }
} }
return filtered; return filtered;
} }
private static String resolveMachineEventTypeFqn(
String machineName,
CodebaseContext context,
List<Transition> machineTransitions) {
if (context != null && machineName != null) {
String[] types = StateMachineTypeResolver.resolve(machineName, context);
if (types != null && types.length > 1 && types[1] != null && !types[1].isBlank()) {
return types[1];
}
}
if (machineTransitions == null) {
return null;
}
for (Transition transition : machineTransitions) {
Event event = transition.getEvent();
if (event == null) {
continue;
}
String fullIdentifier = event.fullIdentifier() != null ? event.fullIdentifier() : event.rawName();
if (fullIdentifier != null && fullIdentifier.contains(".")) {
return fullIdentifier.substring(0, fullIdentifier.lastIndexOf('.'));
}
}
return null;
}
public static List<EntryPoint> filterEntryPointsForMachine( public static List<EntryPoint> filterEntryPointsForMachine(
List<EntryPoint> entryPoints, String machineName, CodebaseContext context) { List<EntryPoint> entryPoints, String machineName, CodebaseContext context) {
if (entryPoints == null || machineName == null) { if (entryPoints == null || machineName == null) {

View File

@@ -63,6 +63,16 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
tp, machineTypes, context, stateMachineTransitions, true); tp, machineTypes, context, stateMachineTransitions, true);
tp = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(tp, machineTypes, context); tp = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(tp, machineTypes, context);
tp = CallChainLinkPolicy.applyRestExternalPolicy(tp, chain, context); tp = CallChainLinkPolicy.applyRestExternalPolicy(tp, chain, context);
if (machineTypes != null) {
tp = tp.toBuilder()
.eventTypeFqn(tp.getEventTypeFqn() != null
? tp.getEventTypeFqn()
: machineTypes.eventTypeFqn())
.stateTypeFqn(tp.getStateTypeFqn() != null
? tp.getStateTypeFqn()
: machineTypes.stateTypeFqn())
.build();
}
if (LifecycleTriggerMarkers.isLifecycle(tp.getEvent())) { if (LifecycleTriggerMarkers.isLifecycle(tp.getEvent())) {
updatedChains.add(chain); updatedChains.add(chain);
@@ -89,7 +99,9 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
Map<String, String> canonicalSourceBySimplified = new LinkedHashMap<>(); Map<String, String> canonicalSourceBySimplified = new LinkedHashMap<>();
for (Transition t : stateMachineTransitions) { for (Transition t : stateMachineTransitions) {
if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp) if (t.getEvent() != null && matchingEngine.matches(t.getEvent(), tp)
&& isRoutedToCorrectMachine(chain, result.getName(), context, stateMachineTransitions) && isRoutedToCorrectMachine(
chain, result.getName(), context, stateMachineTransitions, tp,
machineTypes != null ? machineTypes.eventTypeFqn() : null)
&& isConstraintCompatible(tp.getConstraint(), result.getName())) { && isConstraintCompatible(tp.getConstraint(), result.getName())) {
String smEventForLink = canonicalEvent(t.getEvent()); String smEventForLink = canonicalEvent(t.getEvent());
for (State smSourceState : t.getSourceStates()) { for (State smSourceState : t.getSourceStates()) {
@@ -131,8 +143,10 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
.targetState(smSourceForLink) .targetState(smSourceForLink)
.event(smEventForLink) .event(smEventForLink)
.build(); .build();
if (isRoutedToCorrectMachine(chain, result.getName(), context, stateMachineTransitions) if (isRoutedToCorrectMachine(
&& isConstraintCompatible(tp.getConstraint(), result.getName())) { chain, result.getName(), context, stateMachineTransitions, tp,
machineTypes != null ? machineTypes.eventTypeFqn() : null)
&& isConstraintCompatible(tp.getConstraint(), result.getName())) {
matched.add(mt); matched.add(mt);
} }
} else { } else {
@@ -143,7 +157,9 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
.targetState(targetForLink) .targetState(targetForLink)
.event(smEventForLink) .event(smEventForLink)
.build(); .build();
if (isRoutedToCorrectMachine(chain, result.getName(), context, stateMachineTransitions) if (isRoutedToCorrectMachine(
chain, result.getName(), context, stateMachineTransitions, tp,
machineTypes != null ? machineTypes.eventTypeFqn() : null)
&& isConstraintCompatible(tp.getConstraint(), result.getName())) { && isConstraintCompatible(tp.getConstraint(), result.getName())) {
matched.add(mt); matched.add(mt);
} }
@@ -201,10 +217,26 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
private boolean isRoutedToCorrectMachine( private boolean isRoutedToCorrectMachine(
CallChain chain, String currentMachineName, CodebaseContext context, List<Transition> machineTransitions) { CallChain chain,
String currentMachineName,
CodebaseContext context,
List<Transition> machineTransitions,
TriggerPoint triggerPoint,
String machineEventTypeFqn) {
if (triggerPoint != null
&& machineEventTypeFqn != null
&& CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(triggerPoint, machineEventTypeFqn, context)) {
return true;
}
return routingEngine.hasProvenMachineAffinity(chain, currentMachineName, context, machineTransitions); return routingEngine.hasProvenMachineAffinity(chain, currentMachineName, context, machineTransitions);
} }
private boolean isRoutedToCorrectMachine(
CallChain chain, String currentMachineName, CodebaseContext context, List<Transition> machineTransitions) {
return isRoutedToCorrectMachine(
chain, currentMachineName, context, machineTransitions, chain.getTriggerPoint(), null);
}
private boolean isConstraintCompatible(String constraint, String machineName) { private boolean isConstraintCompatible(String constraint, String machineName) {
if (constraint == null || machineName == null) { if (constraint == null || machineName == null) {
return true; return true;

View File

@@ -131,7 +131,15 @@ public class StrictFqnMatchingEngine implements EventMatchingEngine {
} }
private boolean typesMatch(String type1, String type2) { private boolean typesMatch(String type1, String type2) {
return MachineEnumCanonicalizer.enumTypesMatch(type1, type2, context); if (MachineEnumCanonicalizer.enumTypesMatch(type1, type2, context)) {
return true;
}
if (type1 != null && type2 != null) {
String machineFqn = type1.contains(".") ? type1 : type2;
String candidate = type1.contains(".") ? type2 : type1;
return MachineEnumCanonicalizer.enumTypesMatchForMachine(machineFqn, candidate, context);
}
return false;
} }
private String constantName(String event) { private String constantName(String event) {

View File

@@ -126,8 +126,13 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine {
if (mismatched) { if (mismatched) {
return false; return false;
} }
// Provable generic types but no machine match — fail closed (no package-name fallback). // Fail closed only when both trigger and machine generic types were resolved but
return false; // could not be reconciled. When machine config AST does not expose type args (stub
// config classes in tests, generated configs), fall through to single-SM / package routing.
boolean machineTypesKnown = machineEventFqn != null || machineStateFqn != null;
if (machineTypesKnown) {
return false;
}
} }
} }

View File

@@ -842,18 +842,17 @@ public final class MachineEnumCanonicalizer {
String constant = constantName(stripped); String constant = constantName(stripped);
String typePart = enumTypeFromRef(stripped); String typePart = enumTypeFromRef(stripped);
if (typePart != null && context != null && context.isAmbiguousSimpleName(typePart)) { if (typePart != null && !typePart.contains(".") && importStyleEnumTypeMatches(typePart, enumTypeFqn)
return stripped; && constant.matches("[A-Z_][A-Z0-9_]*")) {
return enumTypeFqn + "." + constant;
} }
if (typePart != null && enumTypesMatch(enumTypeFqn, typePart, context)) { if (typePart != null && enumTypesMatch(enumTypeFqn, typePart, context)) {
return enumTypeFqn + "." + constant; return enumTypeFqn + "." + constant;
} }
if (typePart != null && !typePart.contains(".") && !enumTypesMatch(enumTypeFqn, typePart, context) if (typePart != null && context != null && context.isAmbiguousSimpleName(typePart)) {
&& importStyleEnumTypeMatches(typePart, enumTypeFqn) return stripped;
&& constant.matches("[A-Z_][A-Z0-9_]*")) {
return enumTypeFqn + "." + constant;
} }
if (typePart == null && Character.isUpperCase(stripped.charAt(0)) && !stripped.contains(".")) { if (typePart == null && Character.isUpperCase(stripped.charAt(0)) && !stripped.contains(".")) {
@@ -917,6 +916,47 @@ public final class MachineEnumCanonicalizer {
return enumTypesMatch(type1, type2); return enumTypesMatch(type1, type2);
} }
/**
* Whether a concrete polymorphic enum constant belongs to the configured machine event type.
* Uses machine context to accept import-style labels even when the enum simple name is
* ambiguous across the scanned codebase.
*/
public static boolean polymorphicEventMatchesMachineEventType(
String polymorphicEvent,
String machineEventTypeFqn,
CodebaseContext context) {
if (polymorphicEvent == null || machineEventTypeFqn == null || !polymorphicEvent.contains(".")) {
return false;
}
if (polymorphicEvent.startsWith("<SYMBOLIC:") || polymorphicEvent.startsWith("ENUM_SET:")) {
return false;
}
String enumType = polymorphicEvent.substring(0, polymorphicEvent.lastIndexOf('.'));
if (machineEventTypeFqn.equals(enumType)) {
return true;
}
if (importStyleEnumTypeMatches(enumType, machineEventTypeFqn)) {
return true;
}
return enumType.contains(".") && enumTypesMatch(machineEventTypeFqn, enumType, context);
}
public static boolean enumTypesMatchForMachine(
String machineEnumFqn,
String candidateType,
CodebaseContext context) {
if (machineEnumFqn == null || candidateType == null) {
return false;
}
if (machineEnumFqn.equals(candidateType)) {
return true;
}
if (importStyleEnumTypeMatches(candidateType, machineEnumFqn)) {
return true;
}
return candidateType.contains(".") && enumTypesMatch(machineEnumFqn, candidateType, context);
}
private static String simpleName(String fqn) { private static String simpleName(String fqn) {
return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn; return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn;
} }

View File

@@ -102,4 +102,38 @@ class CallChainLinkPolicyTest {
assertThat(CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(trigger)).isFalse(); assertThat(CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(trigger)).isFalse();
assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(trigger)).isTrue(); assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(trigger)).isTrue();
} }
@Test
void shouldTrustImportStylePolymorphicWidenWhenMachineEventTypeIsKnown() {
TriggerPoint trigger = TriggerPoint.builder()
.ambiguous(true)
.event("OrderEvent.valueOf(eventStr)")
.polymorphicEvents(List.of("OrderEvent.PAY", "OrderEvent.SHIP"))
.build();
assertThat(CallChainLinkPolicy.isTrustedEnumPolymorphicWiden(
trigger, "com.example.order.OrderEvent")).isTrue();
assertThat(CallChainLinkPolicy.shouldFailClosedOnAmbiguousCallGraphWiden(
trigger, "com.example.order.OrderEvent")).isFalse();
}
@Test
void shouldResolveWhenTrustedWidenHasMatchesDespiteAmbiguousCallGraphFlag() {
TriggerPoint trigger = TriggerPoint.builder()
.ambiguous(true)
.event("OrderEvent.valueOf(eventStr)")
.polymorphicEvents(List.of("OrderEvent.PAY", "OrderEvent.SHIP"))
.build();
var matched = List.of(
click.kamil.springstatemachineexporter.analysis.model.MatchedTransition.builder()
.event("com.example.order.OrderEvent.PAY")
.build());
assertThat(CallChainLinkPolicy.resolveLinkResolution(
trigger,
matched,
false,
"com.example.order.OrderEvent")).isEqualTo(LinkResolution.RESOLVED);
}
} }

View File

@@ -0,0 +1,117 @@
package click.kamil.springstatemachineexporter.analysis.enricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.Event;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Regression for enterprise multi-module scans: a globally ambiguous enum simple name must not
* block machine-scoped {@code valueOf} widens from linking when polymorphic events use import-style
* labels and the machine event type is known.
*/
class TrustedEnumWidenLinkingRegressionTest {
@Test
void shouldLinkTrustedImportStyleWidenWhenEnumSimpleNameIsGloballyAmbiguous(@TempDir Path tempDir) throws Exception {
Files.createDirectories(tempDir.resolve("a"));
Files.createDirectories(tempDir.resolve("b"));
Files.writeString(tempDir.resolve("a/OrderEvent.java"),
"package a; public enum OrderEvent { PAY }");
Files.writeString(tempDir.resolve("b/OrderEvent.java"),
"package b; public enum OrderEvent { CANCEL }");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
assertThat(context.isAmbiguousSimpleName("OrderEvent")).isTrue();
Transition pay = new Transition();
pay.setSourceStates(List.of(State.of("NEW", "com.example.order.OrderState.NEW")));
pay.setTargetStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
pay.setEvent(Event.of("PAY", "com.example.order.OrderEvent.PAY"));
Transition ship = new Transition();
ship.setSourceStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
ship.setTargetStates(List.of(State.of("SHIPPED", "com.example.order.OrderState.SHIPPED")));
ship.setEvent(Event.of("SHIP", "com.example.order.OrderEvent.SHIP"));
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.event("OrderEvent.valueOf(eventStr)")
.ambiguous(true)
.external(false)
.polymorphicEvents(List.of("OrderEvent.PAY", "OrderEvent.SHIP"))
.build())
.build();
AnalysisResult result = AnalysisResult.builder()
.name("com.example.config.OrderStateMachineConfiguration")
.eventTypeFqn("com.example.order.OrderEvent")
.stateTypeFqn("com.example.order.OrderState")
.transitions(List.of(pay, ship))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
new TransitionLinkerEnricher().enrich(result, null, null);
CallChain linked = result.getMetadata().getCallChains().get(0);
assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
assertThat(linked.getMatchedTransitions()).hasSizeGreaterThanOrEqualTo(1);
assertThat(linked.getTriggerPoint().getPolymorphicEvents())
.containsExactly("com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent.SHIP");
}
@Test
void shouldKeepTrustedWidenCallChainWhenEnumSimpleNameIsGloballyAmbiguous(@TempDir Path tempDir) throws Exception {
Files.createDirectories(tempDir.resolve("a"));
Files.createDirectories(tempDir.resolve("b"));
Files.writeString(tempDir.resolve("a/OrderEvent.java"),
"package a; public enum OrderEvent { PAY }");
Files.writeString(tempDir.resolve("b/OrderEvent.java"),
"package b; public enum OrderEvent { CANCEL }");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
assertThat(context.isAmbiguousSimpleName("OrderEvent")).isTrue();
Transition pay = new Transition();
pay.setSourceStates(List.of(State.of("NEW", "com.example.order.OrderState.NEW")));
pay.setTargetStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
pay.setEvent(Event.of("PAY", "com.example.order.OrderEvent.PAY"));
Transition ship = new Transition();
ship.setSourceStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
ship.setTargetStates(List.of(State.of("SHIPPED", "com.example.order.OrderState.SHIPPED")));
ship.setEvent(Event.of("SHIP", "com.example.order.OrderEvent.SHIP"));
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.event("OrderEvent.valueOf(eventStr)")
.ambiguous(true)
.external(false)
.polymorphicEvents(List.of("OrderEvent.PAY", "OrderEvent.SHIP"))
.build())
.build();
List<CallChain> scoped = MachineScopeFilter.filterCallChainsForMachine(
List.of(chain),
"com.example.config.OrderStateMachineConfiguration",
context,
List.of(pay, ship));
assertThat(scoped).hasSize(1);
}
}

View File

@@ -340,7 +340,7 @@ class StrictFqnMatchingEngineTest {
} }
@Test @Test
void shouldNotMatchImportStyleEnumWhenSimpleNameIsAmbiguous(@TempDir java.nio.file.Path tempDir) throws Exception { void shouldMatchImportStyleEnumToMachineFqnWhenSimpleNamesAlign(@TempDir java.nio.file.Path tempDir) throws Exception {
java.nio.file.Files.createDirectories(tempDir.resolve("a")); java.nio.file.Files.createDirectories(tempDir.resolve("a"));
java.nio.file.Files.createDirectories(tempDir.resolve("b")); java.nio.file.Files.createDirectories(tempDir.resolve("b"));
java.nio.file.Files.writeString(tempDir.resolve("a/OrderEvent.java"), java.nio.file.Files.writeString(tempDir.resolve("a/OrderEvent.java"),
@@ -359,6 +359,6 @@ class StrictFqnMatchingEngineTest {
.polymorphicEvents(List.of("OrderEvent.PAY")) .polymorphicEvents(List.of("OrderEvent.PAY"))
.build(); .build();
assertThat(engine.matches(smEvent, triggerPoint)).isFalse(); assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
} }
} }

View File

@@ -32,7 +32,7 @@ class MachineEnumCanonicalizerCrossPackageTest {
} }
@Test @Test
void shouldNotRewriteImportStyleWhenSimpleNameIsAmbiguous(@TempDir Path tempDir) throws Exception { void shouldCanonicalizeImportStyleWhenMachineEnumTypeIsKnown(@TempDir Path tempDir) throws Exception {
Files.createDirectories(tempDir.resolve("a")); Files.createDirectories(tempDir.resolve("a"));
Files.createDirectories(tempDir.resolve("b")); Files.createDirectories(tempDir.resolve("b"));
Files.writeString(tempDir.resolve("a/OrderEvent.java"), Files.writeString(tempDir.resolve("a/OrderEvent.java"),
@@ -44,7 +44,7 @@ class MachineEnumCanonicalizerCrossPackageTest {
context.scan(tempDir); context.scan(tempDir);
assertThat(MachineEnumCanonicalizer.canonicalizeLabel("OrderEvent.PAY", "a.OrderEvent", context)) assertThat(MachineEnumCanonicalizer.canonicalizeLabel("OrderEvent.PAY", "a.OrderEvent", context))
.isEqualTo("OrderEvent.PAY"); .isEqualTo("a.OrderEvent.PAY");
} }
@Test @Test
@@ -112,7 +112,7 @@ class MachineEnumCanonicalizerCrossPackageTest {
} }
@Test @Test
void shouldNotCanonicalizeAmbiguousImportStyleTransitionEvents(@TempDir Path tempDir) throws Exception { void shouldCanonicalizeAmbiguousImportStyleTransitionEventsWhenMachineTypeKnown(@TempDir Path tempDir) throws Exception {
Files.createDirectories(tempDir.resolve("a")); Files.createDirectories(tempDir.resolve("a"));
Files.createDirectories(tempDir.resolve("b")); Files.createDirectories(tempDir.resolve("b"));
Files.writeString(tempDir.resolve("a/OrderEvent.java"), Files.writeString(tempDir.resolve("a/OrderEvent.java"),
@@ -138,7 +138,7 @@ class MachineEnumCanonicalizerCrossPackageTest {
"a.OrderState", "a.OrderEvent"); "a.OrderState", "a.OrderEvent");
MachineEnumCanonicalizer.canonicalizeTransitions(List.of(transition), types, context); MachineEnumCanonicalizer.canonicalizeTransitions(List.of(transition), types, context);
assertThat(transition.getEvent().fullIdentifier()).isEqualTo("OrderEvent.PAY"); assertThat(transition.getEvent().fullIdentifier()).isEqualTo("a.OrderEvent.PAY");
assertThat(transition.getSourceStates().get(0).fullIdentifier()).isEqualTo("a.OrderState.NEW"); assertThat(transition.getSourceStates().get(0).fullIdentifier()).isEqualTo("a.OrderState.NEW");
} }
} }

View File

@@ -227,7 +227,7 @@ class AmbiguousSimpleEnumLinkingTest {
} }
@Test @Test
void shouldNotRewriteAmbiguousImportStylePolyDuringTriggerCanonicalization(@TempDir Path tempDir) throws Exception { void shouldCanonicalizeImportStylePolyWhenMachineEventTypeIsKnown(@TempDir Path tempDir) throws Exception {
Files.createDirectories(tempDir.resolve("a")); Files.createDirectories(tempDir.resolve("a"));
Files.createDirectories(tempDir.resolve("b")); Files.createDirectories(tempDir.resolve("b"));
Files.writeString(tempDir.resolve("a/OrderEvent.java"), Files.writeString(tempDir.resolve("a/OrderEvent.java"),
@@ -257,7 +257,7 @@ class AmbiguousSimpleEnumLinkingTest {
TriggerPoint canonical = result.getMetadata().getCallChains().get(0).getTriggerPoint(); TriggerPoint canonical = result.getMetadata().getCallChains().get(0).getTriggerPoint();
assertThat(canonical.getPolymorphicEvents()) assertThat(canonical.getPolymorphicEvents())
.containsExactly("OrderEvent.PAY", "OrderEvent.SHIP"); .containsExactly("a.OrderEvent.PAY", "a.OrderEvent.SHIP");
} }
} }

View File

@@ -124,7 +124,7 @@ class AnalysisResultFinalizerTest {
} }
@Test @Test
void shouldPreserveAmbiguousImportStyleMatchedTransitions(@TempDir Path tempDir) throws Exception { void shouldCanonicalizeAmbiguousImportStyleMatchedTransitions(@TempDir Path tempDir) throws Exception {
Files.createDirectories(tempDir.resolve("a")); Files.createDirectories(tempDir.resolve("a"));
Files.createDirectories(tempDir.resolve("b")); Files.createDirectories(tempDir.resolve("b"));
Files.createDirectories(tempDir.resolve("com/example/config")); Files.createDirectories(tempDir.resolve("com/example/config"));
@@ -165,7 +165,7 @@ class AnalysisResultFinalizerTest {
"a.OrderState", "a.OrderEvent")); "a.OrderState", "a.OrderEvent"));
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getEvent()) assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions().get(0).getEvent())
.isEqualTo("OrderEvent.PAY"); .isEqualTo("a.OrderEvent.PAY");
} }
private static void writeSampleConfig(Path tempDir) throws Exception { private static void writeSampleConfig(Path tempDir) throws Exception {

View File

@@ -738,8 +738,16 @@
"ambiguous" : true "ambiguous" : true
}, },
"contextMachineId" : null, "contextMachineId" : null,
"matchedTransitions" : null, "matchedTransitions" : [ {
"linkResolution" : "AMBIGUOUS_WIDEN" "sourceState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.SUBMITTED",
"targetState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.PAID",
"event" : "click.kamil.examples.statemachine.polymorphic.OrderEvents.PAY"
}, {
"sourceState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.PAID",
"targetState" : "click.kamil.examples.statemachine.polymorphic.OrderStates.CANCELED",
"event" : "click.kamil.examples.statemachine.polymorphic.OrderEvents.ABCD"
} ],
"linkResolution" : "RESOLVED"
} ], } ],
"properties" : { "properties" : {
"default" : { "default" : {