diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/validation/AnalysisCanonicalFormValidator.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/validation/AnalysisCanonicalFormValidator.java index b6939fc..4f17a7f 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/validation/AnalysisCanonicalFormValidator.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/validation/AnalysisCanonicalFormValidator.java @@ -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 violations) { - validateTransitions(result.getTransitions(), machineTypes, violations); - validateStateCollection(result.getStates(), machineTypes.stateTypeFqn(), "states", violations); + List 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 transitions, StateMachineTypeResolver.MachineTypes machineTypes, - List violations) { + List 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 states, String stateTypeFqn, String pathPrefix, - List violations) { + List 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 states, String stateTypeFqn, String pathPrefix, - List violations) { + List 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 labels, String stateTypeFqn, String pathPrefix, - List violations) { + List 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 triggers, StateMachineTypeResolver.MachineTypes machineTypes, - List violations) { + List 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 callChains, List transitions, StateMachineTypeResolver.MachineTypes machineTypes, - List violations) { + List 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 violations) { - if (!MachineEnumCanonicalizer.isMachineEnumReference(value, enumTypeFqn)) { + List 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 transitions, String eventTypeFqn, - List violations) { + List 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 transitions, List 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 transitions, String eventTypeFqn, - List violations) { + List 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 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 polymorphicEvents, List 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; diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/validation/AnalysisCanonicalFormValidatorTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/validation/AnalysisCanonicalFormValidatorTest.java index a93572a..1144de6 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/validation/AnalysisCanonicalFormValidatorTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/validation/AnalysisCanonicalFormValidatorTest.java @@ -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 { + } + """); + + 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");