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:
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user