Thread CodebaseContext through canonical form validator

requireCanonical and event matching now use context-aware enum checks so ambiguous import-style identifiers are not falsely rewritten during export validation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 06:28:56 +02:00
parent be1ac91beb
commit 4a8b6ae9d9
2 changed files with 108 additions and 49 deletions

View File

@@ -50,7 +50,7 @@ public final class AnalysisCanonicalFormValidator {
return violations;
}
validateFields(result, machineTypes, violations);
validateFields(result, machineTypes, violations, context);
return violations;
}
@@ -66,7 +66,7 @@ public final class AnalysisCanonicalFormValidator {
return violations;
}
validateFields(result, machineTypes, violations);
validateFields(result, machineTypes, violations, null);
return violations;
}
@@ -89,17 +89,18 @@ public final class AnalysisCanonicalFormValidator {
private static void validateFields(
AnalysisResult result,
StateMachineTypeResolver.MachineTypes machineTypes,
List<Violation> violations) {
validateTransitions(result.getTransitions(), machineTypes, violations);
validateStateCollection(result.getStates(), machineTypes.stateTypeFqn(), "states", violations);
List<Violation> violations,
CodebaseContext context) {
validateTransitions(result.getTransitions(), machineTypes, violations, context);
validateStateCollection(result.getStates(), machineTypes.stateTypeFqn(), "states", violations, context);
validateStateRawNameSync(result.getStates(), machineTypes.stateTypeFqn(), "states", violations);
validateStateLabels(result.getStartStates(), machineTypes.stateTypeFqn(), "startStates", violations);
validateStateLabels(result.getEndStates(), machineTypes.stateTypeFqn(), "endStates", violations);
validateStateLabels(result.getStartStates(), machineTypes.stateTypeFqn(), "startStates", violations, context);
validateStateLabels(result.getEndStates(), machineTypes.stateTypeFqn(), "endStates", violations, context);
if (result.getMetadata() != null) {
validateTriggers(result.getMetadata().getTriggers(), machineTypes, violations);
validateTriggers(result.getMetadata().getTriggers(), machineTypes, violations, context);
validateCallChains(
result.getMetadata().getCallChains(), result.getTransitions(), machineTypes, violations);
result.getMetadata().getCallChains(), result.getTransitions(), machineTypes, violations, context);
}
}
@@ -114,7 +115,8 @@ public final class AnalysisCanonicalFormValidator {
private static void validateTransitions(
List<Transition> transitions,
StateMachineTypeResolver.MachineTypes machineTypes,
List<Violation> violations) {
List<Violation> violations,
CodebaseContext context) {
if (transitions == null) {
return;
}
@@ -126,7 +128,8 @@ public final class AnalysisCanonicalFormValidator {
prefix + ".event.fullIdentifier",
transition.getEvent().fullIdentifier(),
machineTypes.eventTypeFqn(),
violations);
violations,
context);
requireRawNameSync(
prefix + ".event.rawName",
transition.getEvent().rawName(),
@@ -135,9 +138,9 @@ public final class AnalysisCanonicalFormValidator {
violations);
}
validateStateListWithRawSync(
transition.getSourceStates(), machineTypes.stateTypeFqn(), prefix + ".sourceStates", violations);
transition.getSourceStates(), machineTypes.stateTypeFqn(), prefix + ".sourceStates", violations, context);
validateStateListWithRawSync(
transition.getTargetStates(), machineTypes.stateTypeFqn(), prefix + ".targetStates", violations);
transition.getTargetStates(), machineTypes.stateTypeFqn(), prefix + ".targetStates", violations, context);
}
}
@@ -145,13 +148,14 @@ public final class AnalysisCanonicalFormValidator {
List<State> states,
String stateTypeFqn,
String pathPrefix,
List<Violation> violations) {
List<Violation> violations,
CodebaseContext context) {
if (states == null) {
return;
}
for (int i = 0; i < states.size(); i++) {
State state = states.get(i);
requireCanonical(pathPrefix + "[" + i + "].fullIdentifier", state.fullIdentifier(), stateTypeFqn, violations);
requireCanonical(pathPrefix + "[" + i + "].fullIdentifier", state.fullIdentifier(), stateTypeFqn, violations, context);
requireRawNameSync(
pathPrefix + "[" + i + "].rawName",
state.rawName(),
@@ -183,13 +187,14 @@ public final class AnalysisCanonicalFormValidator {
Set<State> states,
String stateTypeFqn,
String pathPrefix,
List<Violation> violations) {
List<Violation> violations,
CodebaseContext context) {
if (states == null) {
return;
}
int i = 0;
for (State state : states) {
requireCanonical(pathPrefix + "[" + i++ + "].fullIdentifier", state.fullIdentifier(), stateTypeFqn, violations);
requireCanonical(pathPrefix + "[" + i++ + "].fullIdentifier", state.fullIdentifier(), stateTypeFqn, violations, context);
}
}
@@ -197,20 +202,22 @@ public final class AnalysisCanonicalFormValidator {
Set<String> labels,
String stateTypeFqn,
String pathPrefix,
List<Violation> violations) {
List<Violation> violations,
CodebaseContext context) {
if (labels == null) {
return;
}
int i = 0;
for (String label : labels) {
requireCanonical(pathPrefix + "[" + i++ + "]", label, stateTypeFqn, violations);
requireCanonical(pathPrefix + "[" + i++ + "]", label, stateTypeFqn, violations, context);
}
}
private static void validateTriggers(
List<TriggerPoint> triggers,
StateMachineTypeResolver.MachineTypes machineTypes,
List<Violation> violations) {
List<Violation> violations,
CodebaseContext context) {
if (triggers == null) {
return;
}
@@ -220,16 +227,16 @@ public final class AnalysisCanonicalFormValidator {
String eventTypeFqn = preferTypeFqn(trigger.getEventTypeFqn(), machineTypes.eventTypeFqn());
String stateTypeFqn = preferTypeFqn(trigger.getStateTypeFqn(), machineTypes.stateTypeFqn());
requireCanonical(prefix + ".event", trigger.getEvent(), eventTypeFqn, violations);
requireCanonical(prefix + ".event", trigger.getEvent(), eventTypeFqn, violations, context);
validateTriggerEventForm(prefix + ".event", trigger.getEvent(), violations);
requireCanonical(prefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations);
requireCanonical(prefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations, context);
validateDynamicIdentifierForm(prefix + ".sourceState", trigger.getSourceState(), violations);
if (trigger.getPolymorphicEvents() != null) {
for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
String polyPath = prefix + ".polymorphicEvents[" + j + "]";
String polyEvent = trigger.getPolymorphicEvents().get(j);
requireCanonical(polyPath, polyEvent, eventTypeFqn, violations);
requireCanonical(polyPath, polyEvent, eventTypeFqn, violations, context);
validatePolymorphicEventForm(polyPath, polyEvent, violations);
}
}
@@ -240,7 +247,8 @@ public final class AnalysisCanonicalFormValidator {
List<CallChain> callChains,
List<Transition> transitions,
StateMachineTypeResolver.MachineTypes machineTypes,
List<Violation> violations) {
List<Violation> violations,
CodebaseContext context) {
if (callChains == null) {
return;
}
@@ -252,30 +260,30 @@ public final class AnalysisCanonicalFormValidator {
String triggerPrefix = prefix + ".triggerPoint";
String eventTypeFqn = preferTypeFqn(trigger.getEventTypeFqn(), machineTypes.eventTypeFqn());
String stateTypeFqn = preferTypeFqn(trigger.getStateTypeFqn(), machineTypes.stateTypeFqn());
requireCanonical(triggerPrefix + ".event", trigger.getEvent(), eventTypeFqn, violations);
requireCanonical(triggerPrefix + ".event", trigger.getEvent(), eventTypeFqn, violations, context);
validateTriggerEventForm(triggerPrefix + ".event", trigger.getEvent(), violations);
requireCanonical(triggerPrefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations);
requireCanonical(triggerPrefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations, context);
validateDynamicIdentifierForm(triggerPrefix + ".sourceState", trigger.getSourceState(), violations);
if (trigger.getPolymorphicEvents() != null) {
for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
String polyPath = triggerPrefix + ".polymorphicEvents[" + j + "]";
String polyEvent = trigger.getPolymorphicEvents().get(j);
requireCanonical(polyPath, polyEvent, eventTypeFqn, violations);
requireCanonical(polyPath, polyEvent, eventTypeFqn, violations, context);
validatePolymorphicEventForm(polyPath, polyEvent, violations);
}
}
validateMatchedTransitionsWhenResolvable(
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations);
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations, context);
validateOverLinkedPolymorphicEvents(
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations);
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations, context);
}
if (chain.getMatchedTransitions() != null) {
for (int j = 0; j < chain.getMatchedTransitions().size(); j++) {
MatchedTransition matched = chain.getMatchedTransitions().get(j);
String matchedPrefix = prefix + ".matchedTransitions[" + j + "]";
requireCanonical(matchedPrefix + ".event", matched.getEvent(), machineTypes.eventTypeFqn(), violations);
requireCanonical(matchedPrefix + ".sourceState", matched.getSourceState(), machineTypes.stateTypeFqn(), violations);
requireCanonical(matchedPrefix + ".targetState", matched.getTargetState(), machineTypes.stateTypeFqn(), violations);
requireCanonical(matchedPrefix + ".event", matched.getEvent(), machineTypes.eventTypeFqn(), violations, context);
requireCanonical(matchedPrefix + ".sourceState", matched.getSourceState(), machineTypes.stateTypeFqn(), violations, context);
requireCanonical(matchedPrefix + ".targetState", matched.getTargetState(), machineTypes.stateTypeFqn(), violations, context);
}
}
}
@@ -292,11 +300,12 @@ public final class AnalysisCanonicalFormValidator {
String path,
String value,
String enumTypeFqn,
List<Violation> violations) {
if (!MachineEnumCanonicalizer.isMachineEnumReference(value, enumTypeFqn)) {
List<Violation> violations,
CodebaseContext context) {
if (!MachineEnumCanonicalizer.isMachineEnumReference(value, enumTypeFqn, context)) {
return;
}
String expected = MachineEnumCanonicalizer.canonicalizeLabel(value, enumTypeFqn);
String expected = MachineEnumCanonicalizer.canonicalizeLabel(value, enumTypeFqn, context);
if (!expected.equals(value)) {
violations.add(new Violation(path, value, expected));
}
@@ -347,7 +356,8 @@ public final class AnalysisCanonicalFormValidator {
TriggerPoint trigger,
List<Transition> transitions,
String eventTypeFqn,
List<Violation> violations) {
List<Violation> violations,
CodebaseContext context) {
if (trigger.getPolymorphicEvents() == null || trigger.getPolymorphicEvents().isEmpty()) {
return;
}
@@ -373,7 +383,7 @@ public final class AnalysisCanonicalFormValidator {
return;
}
int matchingTransitionCount = countMatchingConfiguredTransitions(
transitions, trigger.getPolymorphicEvents(), eventTypeFqn);
transitions, trigger.getPolymorphicEvents(), eventTypeFqn, context);
if (matchingTransitionCount > 0
&& chain.getMatchedTransitions() != null
&& chain.getMatchedTransitions().size() > matchingTransitionCount) {
@@ -387,7 +397,8 @@ public final class AnalysisCanonicalFormValidator {
private static int countMatchingConfiguredTransitions(
List<Transition> transitions,
List<String> polymorphicEvents,
String eventTypeFqn) {
String eventTypeFqn,
CodebaseContext context) {
if (transitions == null || transitions.isEmpty() || polymorphicEvents == null) {
return 0;
}
@@ -400,7 +411,7 @@ public final class AnalysisCanonicalFormValidator {
? transition.getEvent().fullIdentifier()
: transition.getEvent().rawName();
for (String polyEvent : polymorphicEvents) {
if (eventsMatch(polyEvent, smEvent, eventTypeFqn)) {
if (eventsMatch(polyEvent, smEvent, eventTypeFqn, context)) {
count++;
break;
}
@@ -415,7 +426,8 @@ public final class AnalysisCanonicalFormValidator {
TriggerPoint trigger,
List<Transition> transitions,
String eventTypeFqn,
List<Violation> violations) {
List<Violation> violations,
CodebaseContext context) {
if (LifecycleTriggerMarkers.isLifecycle(trigger.getEvent())) {
return;
}
@@ -429,7 +441,7 @@ public final class AnalysisCanonicalFormValidator {
MachineEnumCanonicalizer.TriggerEventKind eventKind =
MachineEnumCanonicalizer.classifyTriggerEvent(trigger.getEvent());
if (eventKind == MachineEnumCanonicalizer.TriggerEventKind.CANONICAL_ENUM) {
if (eventMatchesAnyTransition(trigger.getEvent(), transitions, eventTypeFqn)) {
if (eventMatchesAnyTransition(trigger.getEvent(), transitions, eventTypeFqn, context)) {
violations.add(new Violation(
chainPrefix + ".matchedTransitions",
"empty",
@@ -444,7 +456,7 @@ public final class AnalysisCanonicalFormValidator {
if (eventKind != MachineEnumCanonicalizer.TriggerEventKind.DYNAMIC_EXPRESSION) {
return;
}
if (!polymorphicEventsMatchAnyTransition(trigger.getPolymorphicEvents(), transitions, eventTypeFqn)) {
if (!polymorphicEventsMatchAnyTransition(trigger.getPolymorphicEvents(), transitions, eventTypeFqn, context)) {
return;
}
violations.add(new Violation(
@@ -476,7 +488,8 @@ public final class AnalysisCanonicalFormValidator {
private static boolean eventMatchesAnyTransition(
String triggerEvent,
List<Transition> transitions,
String eventTypeFqn) {
String eventTypeFqn,
CodebaseContext context) {
if (transitions == null || transitions.isEmpty() || triggerEvent == null) {
return false;
}
@@ -487,7 +500,7 @@ public final class AnalysisCanonicalFormValidator {
String smEvent = transition.getEvent().fullIdentifier() != null
? transition.getEvent().fullIdentifier()
: transition.getEvent().rawName();
if (eventsMatch(triggerEvent, smEvent, eventTypeFqn)) {
if (eventsMatch(triggerEvent, smEvent, eventTypeFqn, context)) {
return true;
}
}
@@ -497,7 +510,8 @@ public final class AnalysisCanonicalFormValidator {
private static boolean polymorphicEventsMatchAnyTransition(
List<String> polymorphicEvents,
List<Transition> transitions,
String eventTypeFqn) {
String eventTypeFqn,
CodebaseContext context) {
if (transitions == null || transitions.isEmpty()) {
return false;
}
@@ -509,7 +523,7 @@ public final class AnalysisCanonicalFormValidator {
String smEvent = transition.getEvent().fullIdentifier() != null
? transition.getEvent().fullIdentifier()
: transition.getEvent().rawName();
if (eventsMatch(polyEvent, smEvent, eventTypeFqn)) {
if (eventsMatch(polyEvent, smEvent, eventTypeFqn, context)) {
return true;
}
}
@@ -517,7 +531,11 @@ public final class AnalysisCanonicalFormValidator {
return false;
}
private static boolean eventsMatch(String triggerEvent, String smEvent, String eventTypeFqn) {
private static boolean eventsMatch(
String triggerEvent,
String smEvent,
String eventTypeFqn,
CodebaseContext context) {
if (triggerEvent == null || smEvent == null) {
return false;
}
@@ -529,8 +547,8 @@ public final class AnalysisCanonicalFormValidator {
if (!triggerConst.equals(smConst)) {
return false;
}
if (MachineEnumCanonicalizer.isMachineEnumReference(triggerEvent, eventTypeFqn)
&& MachineEnumCanonicalizer.isMachineEnumReference(smEvent, eventTypeFqn)) {
if (MachineEnumCanonicalizer.isMachineEnumReference(triggerEvent, eventTypeFqn, context)
&& MachineEnumCanonicalizer.isMachineEnumReference(smEvent, eventTypeFqn, context)) {
return true;
}
return false;

View File

@@ -392,6 +392,47 @@ class AnalysisCanonicalFormValidatorTest {
.noneMatch(v -> v.path().contains("matchedTransitions"));
}
@Test
void shouldSkipImportStyleCanonicalCheckWhenSimpleNameIsAmbiguous(@TempDir Path tempDir) throws IOException {
Files.createDirectories(tempDir.resolve("a"));
Files.createDirectories(tempDir.resolve("b"));
Files.createDirectories(tempDir.resolve("com/example/config"));
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 }");
Files.writeString(tempDir.resolve("a/OrderState.java"), "package a; public enum OrderState { NEW, PAID }");
Files.writeString(tempDir.resolve("com/example/config/OrderStateMachineConfiguration.java"),
"""
package com.example.config;
import a.OrderEvent;
import a.OrderState;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
public class OrderStateMachineConfiguration
extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
}
""");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
AnalysisResult result = AnalysisResult.builder()
.name("com.example.config.OrderStateMachineConfiguration")
.transitions(List.of(transition(
"a.OrderEvent.PAY",
"a.OrderState.NEW",
"a.OrderState.PAID")))
.metadata(CodebaseMetadata.builder()
.triggers(List.of(TriggerPoint.builder()
.event("OrderEvent.PAY")
.className("com.example.web.Controller")
.methodName("pay")
.sourceFile("Controller.java")
.build()))
.build())
.build();
assertThat(AnalysisCanonicalFormValidator.validate(result, context)).isEmpty();
}
@Test
void shouldSkipValidationForStringStateMachines(@TempDir Path tempDir) throws IOException {
Path configPkg = tempDir.resolve("com/example/config");