Fail closed on ambiguous import-style enum canonicalization

Stop rewriting OrderEvent.PAY to the machine package when the simple name is ambiguous, and apply the same guard when capping polymorphic events to configured transitions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 05:52:37 +02:00
parent 26560fee6d
commit cef9dd1f9a
2 changed files with 73 additions and 9 deletions

View File

@@ -69,6 +69,13 @@ public final class MachineEnumCanonicalizer {
public static TriggerPoint canonicalizeTriggerPoint( public static TriggerPoint canonicalizeTriggerPoint(
TriggerPoint trigger, TriggerPoint trigger,
StateMachineTypeResolver.MachineTypes machineTypes) { StateMachineTypeResolver.MachineTypes machineTypes) {
return canonicalizeTriggerPoint(trigger, machineTypes, null);
}
public static TriggerPoint canonicalizeTriggerPoint(
TriggerPoint trigger,
StateMachineTypeResolver.MachineTypes machineTypes,
CodebaseContext context) {
if (trigger == null) { if (trigger == null) {
return null; return null;
} }
@@ -81,14 +88,14 @@ public final class MachineEnumCanonicalizer {
List<String> polymorphicEvents = trigger.getPolymorphicEvents() == null ? null List<String> polymorphicEvents = trigger.getPolymorphicEvents() == null ? null
: trigger.getPolymorphicEvents().stream() : trigger.getPolymorphicEvents().stream()
.map(event -> canonicalizeLabel(event, eventTypeFqn)) .map(event -> canonicalizeLabel(event, eventTypeFqn, context))
.collect(Collectors.toList()); .collect(Collectors.toList());
return trigger.toBuilder() return trigger.toBuilder()
.eventTypeFqn(eventTypeFqn) .eventTypeFqn(eventTypeFqn)
.stateTypeFqn(stateTypeFqn) .stateTypeFqn(stateTypeFqn)
.event(canonicalizeLabel(trigger.getEvent(), eventTypeFqn)) .event(canonicalizeLabel(trigger.getEvent(), eventTypeFqn, context))
.sourceState(canonicalizeLabel(trigger.getSourceState(), stateTypeFqn)) .sourceState(canonicalizeLabel(trigger.getSourceState(), stateTypeFqn, context))
.polymorphicEvents(polymorphicEvents) .polymorphicEvents(polymorphicEvents)
.build(); .build();
} }
@@ -162,7 +169,7 @@ public final class MachineEnumCanonicalizer {
TriggerPoint trigger, TriggerPoint trigger,
StateMachineTypeResolver.MachineTypes machineTypes, StateMachineTypeResolver.MachineTypes machineTypes,
CodebaseContext context) { CodebaseContext context) {
TriggerPoint canonical = canonicalizeTriggerPoint(trigger, machineTypes); TriggerPoint canonical = canonicalizeTriggerPoint(trigger, machineTypes, context);
if (canonical == null || machineTypes == null || machineTypes.eventTypeFqn() == null) { if (canonical == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
return canonical; return canonical;
} }
@@ -404,7 +411,7 @@ public final class MachineEnumCanonicalizer {
} }
if (!transitionEvents.isEmpty()) { if (!transitionEvents.isEmpty()) {
result = capToConfiguredTransitionEvents(result, transitionEvents, eventTypeFqn); result = capToConfiguredTransitionEvents(result, transitionEvents, eventTypeFqn, context);
} }
return result; return result;
} }
@@ -416,6 +423,14 @@ public final class MachineEnumCanonicalizer {
List<String> candidates, List<String> candidates,
List<String> transitionEvents, List<String> transitionEvents,
String eventTypeFqn) { String eventTypeFqn) {
return capToConfiguredTransitionEvents(candidates, transitionEvents, eventTypeFqn, null);
}
static List<String> capToConfiguredTransitionEvents(
List<String> candidates,
List<String> transitionEvents,
String eventTypeFqn,
CodebaseContext context) {
if (transitionEvents == null || transitionEvents.isEmpty()) { if (transitionEvents == null || transitionEvents.isEmpty()) {
return candidates == null ? List.of() : candidates; return candidates == null ? List.of() : candidates;
} }
@@ -440,12 +455,12 @@ public final class MachineEnumCanonicalizer {
String transitionType = enumTypeFromRef(transitionEvent); String transitionType = enumTypeFromRef(transitionEvent);
if (candidate.contains(".") && transitionEvent.contains(".")) { if (candidate.contains(".") && transitionEvent.contains(".")) {
if (candidateType != null && transitionType != null if (candidateType != null && transitionType != null
&& enumTypesMatch(candidateType, transitionType)) { && enumTypesMatch(candidateType, transitionType, context)) {
capped.add(transitionEvent); capped.add(transitionEvent);
break; break;
} }
} else if (eventTypeFqn != null && transitionType != null } else if (eventTypeFqn != null && transitionType != null
&& enumTypesMatch(eventTypeFqn, transitionType)) { && enumTypesMatch(eventTypeFqn, transitionType, context)) {
capped.add(transitionEvent); capped.add(transitionEvent);
break; break;
} }
@@ -723,6 +738,10 @@ public final class MachineEnumCanonicalizer {
} }
public static String canonicalizeLabel(String value, String enumTypeFqn) { public static String canonicalizeLabel(String value, String enumTypeFqn) {
return canonicalizeLabel(value, enumTypeFqn, null);
}
public static String canonicalizeLabel(String value, String enumTypeFqn, CodebaseContext context) {
if (value == null || value.isBlank() || enumTypeFqn == null || enumTypeFqn.isBlank()) { if (value == null || value.isBlank() || enumTypeFqn == null || enumTypeFqn.isBlank()) {
return value; return value;
} }
@@ -749,11 +768,15 @@ public final class MachineEnumCanonicalizer {
String constant = constantName(stripped); String constant = constantName(stripped);
String typePart = enumTypeFromRef(stripped); String typePart = enumTypeFromRef(stripped);
if (typePart != null && enumTypesMatch(enumTypeFqn, typePart)) { if (typePart != null && context != null && context.isAmbiguousSimpleName(typePart)) {
return stripped;
}
if (typePart != null && enumTypesMatch(enumTypeFqn, typePart, context)) {
return enumTypeFqn + "." + constant; return enumTypeFqn + "." + constant;
} }
if (typePart != null && !typePart.contains(".") && !enumTypesMatch(enumTypeFqn, typePart) if (typePart != null && !typePart.contains(".") && !enumTypesMatch(enumTypeFqn, typePart, context)
&& importStyleEnumTypeMatches(typePart, enumTypeFqn) && importStyleEnumTypeMatches(typePart, enumTypeFqn)
&& constant.matches("[A-Z_][A-Z0-9_]*")) { && constant.matches("[A-Z_][A-Z0-9_]*")) {
return enumTypeFqn + "." + constant; return enumTypeFqn + "." + constant;

View File

@@ -1,6 +1,12 @@
package click.kamil.springstatemachineexporter.analysis.resolver; package click.kamil.springstatemachineexporter.analysis.resolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test; 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; import static org.assertj.core.api.Assertions.assertThat;
@@ -23,4 +29,39 @@ class MachineEnumCanonicalizerCrossPackageTest {
"OrderEvents.PAY", "com.bar.OrderEvents")) "OrderEvents.PAY", "com.bar.OrderEvents"))
.isEqualTo("com.bar.OrderEvents.PAY"); .isEqualTo("com.bar.OrderEvents.PAY");
} }
@Test
void shouldNotRewriteImportStyleWhenSimpleNameIsAmbiguous(@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(MachineEnumCanonicalizer.canonicalizeLabel("OrderEvent.PAY", "a.OrderEvent", context))
.isEqualTo("OrderEvent.PAY");
}
@Test
void shouldNotCapImportStyleWhenSimpleNameIsAmbiguous(@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(MachineEnumCanonicalizer.capToConfiguredTransitionEvents(
List.of("OrderEvent.PAY"),
List.of("a.OrderEvent.PAY"),
"a.OrderEvent",
context)).isEmpty();
}
} }