Harden enum identifier validation for rawName sync, polymorphic events, and matched transitions.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -471,6 +471,65 @@ public final class MachineEnumCanonicalizer {
|
||||
/**
|
||||
* Derives {@code EnumFormat.fn} display form ({@code OrderEvent.PAY}) from a package-canonical identifier.
|
||||
*/
|
||||
public static String expectedFnRawName(String canonicalFqn, String enumTypeFqn) {
|
||||
return toFnForm(canonicalFqn, enumTypeFqn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether {@code rawName} is the fn-form counterpart of a package-canonical {@code fullIdentifier}.
|
||||
*/
|
||||
public static boolean isRawNameConsistentWithFullIdentifier(
|
||||
String rawName,
|
||||
String fullIdentifier,
|
||||
String enumTypeFqn) {
|
||||
if (fullIdentifier == null || fullIdentifier.isBlank() || isStringOrPrimitiveType(enumTypeFqn)) {
|
||||
return true;
|
||||
}
|
||||
if (fullIdentifier.startsWith("<") || fullIdentifier.startsWith("\"")) {
|
||||
return true;
|
||||
}
|
||||
if (isDynamicTriggerExpression(fullIdentifier)) {
|
||||
return true;
|
||||
}
|
||||
String canonical = canonicalizeLabel(fullIdentifier, enumTypeFqn);
|
||||
if (!canonical.equals(fullIdentifier)) {
|
||||
return true;
|
||||
}
|
||||
if (!constantName(fullIdentifier).matches("[A-Z_][A-Z0-9_]*")) {
|
||||
return true;
|
||||
}
|
||||
String expectedRaw = toFnForm(fullIdentifier, enumTypeFqn);
|
||||
if (expectedRaw == null) {
|
||||
return true;
|
||||
}
|
||||
return expectedRaw.equals(rawName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Polymorphic event lists must contain only package-canonical enum constants or symbolic wildcards.
|
||||
*/
|
||||
public static boolean isValidPolymorphicEventForm(String event) {
|
||||
if (event == null || event.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
TriggerEventKind kind = classifyTriggerEvent(event);
|
||||
if (kind != TriggerEventKind.CANONICAL_ENUM && kind != TriggerEventKind.SYMBOLIC) {
|
||||
return false;
|
||||
}
|
||||
return !hasCorruptedPackageDynamicEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when every entry is a concrete package-canonical enum constant (not symbolic/dynamic).
|
||||
*/
|
||||
public static boolean hasOnlyConcretePolymorphicEvents(List<String> polymorphicEvents) {
|
||||
if (polymorphicEvents == null || polymorphicEvents.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return polymorphicEvents.stream()
|
||||
.allMatch(pe -> classifyTriggerEvent(pe) == TriggerEventKind.CANONICAL_ENUM);
|
||||
}
|
||||
|
||||
static String toFnForm(String canonicalFqn, String enumTypeFqn) {
|
||||
if (canonicalFqn == null || canonicalFqn.isBlank() || isStringOrPrimitiveType(enumTypeFqn)) {
|
||||
return null;
|
||||
|
||||
@@ -2,6 +2,7 @@ package click.kamil.springstatemachineexporter.analysis.validation;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.LifecycleTriggerMarkers;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
@@ -91,12 +92,14 @@ public final class AnalysisCanonicalFormValidator {
|
||||
List<Violation> violations) {
|
||||
validateTransitions(result.getTransitions(), machineTypes, violations);
|
||||
validateStateCollection(result.getStates(), machineTypes.stateTypeFqn(), "states", violations);
|
||||
validateStateRawNameSync(result.getStates(), machineTypes.stateTypeFqn(), "states", violations);
|
||||
validateStateLabels(result.getStartStates(), machineTypes.stateTypeFqn(), "startStates", violations);
|
||||
validateStateLabels(result.getEndStates(), machineTypes.stateTypeFqn(), "endStates", violations);
|
||||
|
||||
if (result.getMetadata() != null) {
|
||||
validateTriggers(result.getMetadata().getTriggers(), machineTypes, violations);
|
||||
validateCallChains(result.getMetadata().getCallChains(), machineTypes, violations);
|
||||
validateCallChains(
|
||||
result.getMetadata().getCallChains(), result.getTransitions(), machineTypes, violations);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,13 +127,21 @@ public final class AnalysisCanonicalFormValidator {
|
||||
transition.getEvent().fullIdentifier(),
|
||||
machineTypes.eventTypeFqn(),
|
||||
violations);
|
||||
requireRawNameSync(
|
||||
prefix + ".event.rawName",
|
||||
transition.getEvent().rawName(),
|
||||
transition.getEvent().fullIdentifier(),
|
||||
machineTypes.eventTypeFqn(),
|
||||
violations);
|
||||
}
|
||||
validateStateList(transition.getSourceStates(), machineTypes.stateTypeFqn(), prefix + ".sourceStates", violations);
|
||||
validateStateList(transition.getTargetStates(), machineTypes.stateTypeFqn(), prefix + ".targetStates", violations);
|
||||
validateStateListWithRawSync(
|
||||
transition.getSourceStates(), machineTypes.stateTypeFqn(), prefix + ".sourceStates", violations);
|
||||
validateStateListWithRawSync(
|
||||
transition.getTargetStates(), machineTypes.stateTypeFqn(), prefix + ".targetStates", violations);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateStateList(
|
||||
private static void validateStateListWithRawSync(
|
||||
List<State> states,
|
||||
String stateTypeFqn,
|
||||
String pathPrefix,
|
||||
@@ -139,7 +150,32 @@ public final class AnalysisCanonicalFormValidator {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < states.size(); i++) {
|
||||
requireCanonical(pathPrefix + "[" + i + "].fullIdentifier", states.get(i).fullIdentifier(), stateTypeFqn, violations);
|
||||
State state = states.get(i);
|
||||
requireCanonical(pathPrefix + "[" + i + "].fullIdentifier", state.fullIdentifier(), stateTypeFqn, violations);
|
||||
requireRawNameSync(
|
||||
pathPrefix + "[" + i + "].rawName",
|
||||
state.rawName(),
|
||||
state.fullIdentifier(),
|
||||
stateTypeFqn,
|
||||
violations);
|
||||
}
|
||||
}
|
||||
private static void validateStateRawNameSync(
|
||||
Set<State> states,
|
||||
String stateTypeFqn,
|
||||
String pathPrefix,
|
||||
List<Violation> violations) {
|
||||
if (states == null) {
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
for (State state : states) {
|
||||
requireRawNameSync(
|
||||
pathPrefix + "[" + i++ + "].rawName",
|
||||
state.rawName(),
|
||||
state.fullIdentifier(),
|
||||
stateTypeFqn,
|
||||
violations);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,14 +223,14 @@ public final class AnalysisCanonicalFormValidator {
|
||||
requireCanonical(prefix + ".event", trigger.getEvent(), eventTypeFqn, violations);
|
||||
validateTriggerEventForm(prefix + ".event", trigger.getEvent(), violations);
|
||||
requireCanonical(prefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations);
|
||||
validateDynamicIdentifierForm(prefix + ".sourceState", trigger.getSourceState(), violations);
|
||||
|
||||
if (trigger.getPolymorphicEvents() != null) {
|
||||
for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
|
||||
requireCanonical(
|
||||
prefix + ".polymorphicEvents[" + j + "]",
|
||||
trigger.getPolymorphicEvents().get(j),
|
||||
eventTypeFqn,
|
||||
violations);
|
||||
String polyPath = prefix + ".polymorphicEvents[" + j + "]";
|
||||
String polyEvent = trigger.getPolymorphicEvents().get(j);
|
||||
requireCanonical(polyPath, polyEvent, eventTypeFqn, violations);
|
||||
validatePolymorphicEventForm(polyPath, polyEvent, violations);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,6 +238,7 @@ public final class AnalysisCanonicalFormValidator {
|
||||
|
||||
private static void validateCallChains(
|
||||
List<CallChain> callChains,
|
||||
List<Transition> transitions,
|
||||
StateMachineTypeResolver.MachineTypes machineTypes,
|
||||
List<Violation> violations) {
|
||||
if (callChains == null) {
|
||||
@@ -218,15 +255,17 @@ public final class AnalysisCanonicalFormValidator {
|
||||
requireCanonical(triggerPrefix + ".event", trigger.getEvent(), eventTypeFqn, violations);
|
||||
validateTriggerEventForm(triggerPrefix + ".event", trigger.getEvent(), violations);
|
||||
requireCanonical(triggerPrefix + ".sourceState", trigger.getSourceState(), stateTypeFqn, violations);
|
||||
validateDynamicIdentifierForm(triggerPrefix + ".sourceState", trigger.getSourceState(), violations);
|
||||
if (trigger.getPolymorphicEvents() != null) {
|
||||
for (int j = 0; j < trigger.getPolymorphicEvents().size(); j++) {
|
||||
requireCanonical(
|
||||
triggerPrefix + ".polymorphicEvents[" + j + "]",
|
||||
trigger.getPolymorphicEvents().get(j),
|
||||
eventTypeFqn,
|
||||
violations);
|
||||
String polyPath = triggerPrefix + ".polymorphicEvents[" + j + "]";
|
||||
String polyEvent = trigger.getPolymorphicEvents().get(j);
|
||||
requireCanonical(polyPath, polyEvent, eventTypeFqn, violations);
|
||||
validatePolymorphicEventForm(polyPath, polyEvent, violations);
|
||||
}
|
||||
}
|
||||
validateMatchedTransitionsWhenResolvable(
|
||||
prefix, chain, trigger, transitions, machineTypes.eventTypeFqn(), violations);
|
||||
}
|
||||
if (chain.getMatchedTransitions() != null) {
|
||||
for (int j = 0; j < chain.getMatchedTransitions().size(); j++) {
|
||||
@@ -261,6 +300,124 @@ public final class AnalysisCanonicalFormValidator {
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireRawNameSync(
|
||||
String path,
|
||||
String rawName,
|
||||
String fullIdentifier,
|
||||
String enumTypeFqn,
|
||||
List<Violation> violations) {
|
||||
if (MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(rawName, fullIdentifier, enumTypeFqn)) {
|
||||
return;
|
||||
}
|
||||
String expected = MachineEnumCanonicalizer.expectedFnRawName(fullIdentifier, enumTypeFqn);
|
||||
violations.add(new Violation(path, rawName, expected));
|
||||
}
|
||||
|
||||
private static void validatePolymorphicEventForm(String path, String event, List<Violation> violations) {
|
||||
if (event == null || event.isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (!MachineEnumCanonicalizer.isValidPolymorphicEventForm(event)) {
|
||||
MachineEnumCanonicalizer.TriggerEventKind kind = MachineEnumCanonicalizer.classifyTriggerEvent(event);
|
||||
String expected = kind == MachineEnumCanonicalizer.TriggerEventKind.DYNAMIC_EXPRESSION
|
||||
&& MachineEnumCanonicalizer.hasCorruptedPackageDynamicEvent(event)
|
||||
? MachineEnumCanonicalizer.denormalizeCorruptedDynamicEvent(event)
|
||||
: "CANONICAL_ENUM or SYMBOLIC";
|
||||
violations.add(new Violation(path, event, expected));
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateDynamicIdentifierForm(String path, String value, List<Violation> violations) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (MachineEnumCanonicalizer.hasCorruptedPackageDynamicEvent(value)) {
|
||||
violations.add(new Violation(
|
||||
path,
|
||||
value,
|
||||
MachineEnumCanonicalizer.denormalizeCorruptedDynamicEvent(value)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateMatchedTransitionsWhenResolvable(
|
||||
String chainPrefix,
|
||||
CallChain chain,
|
||||
TriggerPoint trigger,
|
||||
List<Transition> transitions,
|
||||
String eventTypeFqn,
|
||||
List<Violation> violations) {
|
||||
if (trigger.isExternal() || trigger.isAmbiguous()) {
|
||||
return;
|
||||
}
|
||||
if (LifecycleTriggerMarkers.isLifecycle(trigger.getEvent())) {
|
||||
return;
|
||||
}
|
||||
if (!MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(trigger.getPolymorphicEvents())) {
|
||||
return;
|
||||
}
|
||||
if (MachineEnumCanonicalizer.classifyTriggerEvent(trigger.getEvent())
|
||||
!= MachineEnumCanonicalizer.TriggerEventKind.DYNAMIC_EXPRESSION) {
|
||||
return;
|
||||
}
|
||||
if (chain.getMatchedTransitions() != null && !chain.getMatchedTransitions().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!polymorphicEventsMatchAnyTransition(trigger.getPolymorphicEvents(), transitions, eventTypeFqn)) {
|
||||
return;
|
||||
}
|
||||
violations.add(new Violation(
|
||||
chainPrefix + ".matchedTransitions",
|
||||
"empty",
|
||||
"non-empty when dynamic trigger has concrete polymorphicEvents matching transitions"));
|
||||
}
|
||||
|
||||
private static boolean polymorphicEventsMatchAnyTransition(
|
||||
List<String> polymorphicEvents,
|
||||
List<Transition> transitions,
|
||||
String eventTypeFqn) {
|
||||
if (transitions == null || transitions.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (String polyEvent : polymorphicEvents) {
|
||||
for (Transition transition : transitions) {
|
||||
if (transition.getEvent() == null) {
|
||||
continue;
|
||||
}
|
||||
String smEvent = transition.getEvent().fullIdentifier() != null
|
||||
? transition.getEvent().fullIdentifier()
|
||||
: transition.getEvent().rawName();
|
||||
if (eventsMatch(polyEvent, smEvent, eventTypeFqn)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean eventsMatch(String triggerEvent, String smEvent, String eventTypeFqn) {
|
||||
if (triggerEvent == null || smEvent == null) {
|
||||
return false;
|
||||
}
|
||||
if (triggerEvent.equals(smEvent)) {
|
||||
return true;
|
||||
}
|
||||
String triggerConst = constantName(triggerEvent);
|
||||
String smConst = constantName(smEvent);
|
||||
if (!triggerConst.equals(smConst)) {
|
||||
return false;
|
||||
}
|
||||
if (MachineEnumCanonicalizer.isMachineEnumReference(triggerEvent, eventTypeFqn)
|
||||
&& MachineEnumCanonicalizer.isMachineEnumReference(smEvent, eventTypeFqn)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String constantName(String ref) {
|
||||
int dot = ref.lastIndexOf('.');
|
||||
return dot >= 0 ? ref.substring(dot + 1) : ref;
|
||||
}
|
||||
|
||||
private static void validateTriggerEventForm(String path, String event, List<Violation> violations) {
|
||||
if (event == null || event.isBlank()) {
|
||||
return;
|
||||
|
||||
@@ -211,6 +211,28 @@ class MachineEnumCanonicalizerTest {
|
||||
"com.example.order.OrderEvent.PAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateRawNameConsistencyForCanonicalHelpers() {
|
||||
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
|
||||
"OrderEvent.PAY", "com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent"))
|
||||
.isTrue();
|
||||
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
|
||||
"UNDEFINED", "com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent"))
|
||||
.isFalse();
|
||||
assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm("com.example.order.OrderEvent.PAY"))
|
||||
.isTrue();
|
||||
assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm("<SYMBOLIC: OrderEvent.*>"))
|
||||
.isTrue();
|
||||
assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm("OrderEvent.valueOf(event)"))
|
||||
.isFalse();
|
||||
assertThat(MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(
|
||||
List.of("com.example.order.OrderEvent.PAY")))
|
||||
.isTrue();
|
||||
assertThat(MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(
|
||||
List.of("<SYMBOLIC: OrderEvent.*>")))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDedupeStatesByCanonicalFullIdentifier(@TempDir Path tempDir) throws IOException {
|
||||
writeSampleConfig(tempDir);
|
||||
|
||||
@@ -82,9 +82,83 @@ class ExportPipelineIdentifierConsistencyTest {
|
||||
.resolve("com.example.config.OrderStateMachineConfiguration.json"));
|
||||
|
||||
assertNoMixedEnumIdentifiers(exported);
|
||||
assertRawNameSync(exported);
|
||||
assertThat(exported.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNotEmpty();
|
||||
}
|
||||
|
||||
static void assertRawNameSync(AnalysisResult result) {
|
||||
String stateTypeFqn = result.getStateTypeFqn();
|
||||
String eventTypeFqn = result.getEventTypeFqn();
|
||||
if (stateTypeFqn == null || !stateTypeFqn.contains(".")
|
||||
|| MachineEnumCanonicalizer.isStringOrPrimitiveType(stateTypeFqn)) {
|
||||
return;
|
||||
}
|
||||
if (result.getStates() != null) {
|
||||
for (State state : result.getStates()) {
|
||||
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
|
||||
state.rawName(), state.fullIdentifier(), stateTypeFqn))
|
||||
.as("state rawName sync for %s", state.fullIdentifier())
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
if (result.getTransitions() != null) {
|
||||
for (Transition transition : result.getTransitions()) {
|
||||
if (transition.getEvent() != null) {
|
||||
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
|
||||
transition.getEvent().rawName(),
|
||||
transition.getEvent().fullIdentifier(),
|
||||
eventTypeFqn))
|
||||
.as("event rawName sync for %s", transition.getEvent().fullIdentifier())
|
||||
.isTrue();
|
||||
}
|
||||
assertStateListRawSync(transition.getSourceStates(), stateTypeFqn);
|
||||
assertStateListRawSync(transition.getTargetStates(), stateTypeFqn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertStateListRawSync(List<State> states, String stateTypeFqn) {
|
||||
if (states == null) {
|
||||
return;
|
||||
}
|
||||
for (State state : states) {
|
||||
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
|
||||
state.rawName(), state.fullIdentifier(), stateTypeFqn))
|
||||
.as("state rawName sync for %s", state.fullIdentifier())
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertPolymorphicEventForms(AnalysisResult result) {
|
||||
if (result.getMetadata() == null) {
|
||||
return;
|
||||
}
|
||||
collectTriggers(result).forEach(trigger -> {
|
||||
if (trigger.getPolymorphicEvents() != null) {
|
||||
for (String poly : trigger.getPolymorphicEvents()) {
|
||||
assertThat(MachineEnumCanonicalizer.isValidPolymorphicEventForm(poly))
|
||||
.as("polymorphic event form: %s", poly)
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static List<TriggerPoint> collectTriggers(AnalysisResult result) {
|
||||
List<TriggerPoint> triggers = new java.util.ArrayList<>();
|
||||
if (result.getMetadata().getTriggers() != null) {
|
||||
triggers.addAll(result.getMetadata().getTriggers());
|
||||
}
|
||||
if (result.getMetadata().getCallChains() != null) {
|
||||
for (CallChain chain : result.getMetadata().getCallChains()) {
|
||||
if (chain.getTriggerPoint() != null) {
|
||||
triggers.add(chain.getTriggerPoint());
|
||||
}
|
||||
}
|
||||
}
|
||||
return triggers;
|
||||
}
|
||||
|
||||
@Test
|
||||
void astExportShouldNotMixShortAndPackageCanonicalStateIdentifiers(@TempDir Path tempDir) throws Exception {
|
||||
Path enterprise = findProjectRoot().resolve("state_machines/state_machine_enterprise");
|
||||
@@ -105,7 +179,9 @@ class ExportPipelineIdentifierConsistencyTest {
|
||||
}
|
||||
AnalysisResult result = new JsonImportService().importAnalysisResult(json);
|
||||
assertNoMixedEnumIdentifiers(result);
|
||||
assertRawNameSync(result);
|
||||
assertTriggerEventForms(result);
|
||||
assertPolymorphicEventForms(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,151 @@ class AnalysisCanonicalFormValidatorTest {
|
||||
.noneMatch(v -> v.path().contains(".event"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenEventRawNameDoesNotMatchFnForm() {
|
||||
Transition transition = new Transition();
|
||||
transition.setEvent(Event.of("UNDEFINED", "com.example.order.OrderEvent.PAY"));
|
||||
transition.setSourceStates(List.of(
|
||||
State.of("OrderState.NEW", "com.example.order.OrderState.NEW")));
|
||||
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.transitions(List.of(transition))
|
||||
.build();
|
||||
|
||||
List<AnalysisCanonicalFormValidator.Violation> violations =
|
||||
AnalysisCanonicalFormValidator.validateWithMachineTypes(
|
||||
result,
|
||||
new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
|
||||
"com.example.order.OrderState", "com.example.order.OrderEvent"));
|
||||
|
||||
assertThat(violations)
|
||||
.extracting(AnalysisCanonicalFormValidator.Violation::path)
|
||||
.contains("transitions[0].event.rawName");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenPolymorphicEventIsDynamicExpression() {
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.triggers(List.of(TriggerPoint.builder()
|
||||
.event("OrderEvent.valueOf(eventString)")
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("dispatch")
|
||||
.sourceFile("OrderController.java")
|
||||
.polymorphicEvents(List.of("OrderEvent.valueOf(eventString)"))
|
||||
.build()))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
List<AnalysisCanonicalFormValidator.Violation> violations =
|
||||
AnalysisCanonicalFormValidator.validateWithMachineTypes(
|
||||
result,
|
||||
new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
|
||||
"com.example.order.OrderState", "com.example.order.OrderEvent"));
|
||||
|
||||
assertThat(violations)
|
||||
.extracting(AnalysisCanonicalFormValidator.Violation::path)
|
||||
.contains("metadata.triggers[0].polymorphicEvents[0]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenSourceStateHasCorruptedPackageValueOfForm() {
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.triggers(List.of(TriggerPoint.builder()
|
||||
.event("com.example.order.OrderEvent.PAY")
|
||||
.sourceState("com.example.order.OrderState.valueOf(stateString)")
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("dispatch")
|
||||
.sourceFile("OrderController.java")
|
||||
.build()))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
List<AnalysisCanonicalFormValidator.Violation> violations =
|
||||
AnalysisCanonicalFormValidator.validateWithMachineTypes(
|
||||
result,
|
||||
new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
|
||||
"com.example.order.OrderState", "com.example.order.OrderEvent"));
|
||||
|
||||
assertThat(violations)
|
||||
.extracting(AnalysisCanonicalFormValidator.Violation::path)
|
||||
.contains("metadata.triggers[0].sourceState");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenDynamicTriggerHasConcretePolymorphicEventsButNoMatchedTransitions() {
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.transitions(List.of(transition(
|
||||
"com.example.order.OrderEvent.PAY",
|
||||
"com.example.order.OrderState.NEW",
|
||||
"com.example.order.OrderState.PAID")))
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.callChains(List.of(CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.event("event.getType()")
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("dispatch")
|
||||
.sourceFile("OrderController.java")
|
||||
.polymorphicEvents(List.of("com.example.order.OrderEvent.PAY"))
|
||||
.build())
|
||||
.build()))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
List<AnalysisCanonicalFormValidator.Violation> violations =
|
||||
AnalysisCanonicalFormValidator.validateWithMachineTypes(
|
||||
result,
|
||||
new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
|
||||
"com.example.order.OrderState", "com.example.order.OrderEvent"));
|
||||
|
||||
assertThat(violations)
|
||||
.extracting(AnalysisCanonicalFormValidator.Violation::path)
|
||||
.contains("metadata.callChains[0].matchedTransitions");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowSymbolicPolymorphicEventsWithoutMatchedTransitions() {
|
||||
AnalysisResult result = AnalysisResult.builder()
|
||||
.name("com.example.config.OrderStateMachineConfiguration")
|
||||
.stateTypeFqn("com.example.order.OrderState")
|
||||
.eventTypeFqn("com.example.order.OrderEvent")
|
||||
.transitions(List.of(transition(
|
||||
"com.example.order.OrderEvent.PAY",
|
||||
"com.example.order.OrderState.NEW",
|
||||
"com.example.order.OrderState.PAID")))
|
||||
.metadata(CodebaseMetadata.builder()
|
||||
.callChains(List.of(CallChain.builder()
|
||||
.triggerPoint(TriggerPoint.builder()
|
||||
.event("event.getType()")
|
||||
.className("com.example.web.OrderController")
|
||||
.methodName("dispatch")
|
||||
.sourceFile("OrderController.java")
|
||||
.polymorphicEvents(List.of("<SYMBOLIC: OrderEvent.*>"))
|
||||
.build())
|
||||
.build()))
|
||||
.build())
|
||||
.build();
|
||||
|
||||
assertThat(AnalysisCanonicalFormValidator.validateWithMachineTypes(
|
||||
result,
|
||||
new click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes(
|
||||
"com.example.order.OrderState", "com.example.order.OrderEvent")))
|
||||
.noneMatch(v -> v.path().contains("matchedTransitions"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipValidationForStringStateMachines(@TempDir Path tempDir) throws IOException {
|
||||
Path configPkg = tempDir.resolve("com/example/config");
|
||||
@@ -195,13 +340,25 @@ class AnalysisCanonicalFormValidatorTest {
|
||||
}
|
||||
|
||||
private static Transition transition(String eventFqn, String sourceFqn, String targetFqn) {
|
||||
String eventRaw = fnForm(eventFqn);
|
||||
String sourceRaw = fnForm(sourceFqn);
|
||||
String targetRaw = fnForm(targetFqn);
|
||||
Transition transition = new Transition();
|
||||
transition.setEvent(Event.of(eventFqn, eventFqn));
|
||||
transition.setSourceStates(List.of(State.of(sourceFqn, sourceFqn)));
|
||||
transition.setTargetStates(List.of(State.of(targetFqn, targetFqn)));
|
||||
transition.setEvent(Event.of(eventRaw, eventFqn));
|
||||
transition.setSourceStates(List.of(State.of(sourceRaw, sourceFqn)));
|
||||
transition.setTargetStates(List.of(State.of(targetRaw, targetFqn)));
|
||||
return transition;
|
||||
}
|
||||
|
||||
private static String fnForm(String identifier) {
|
||||
if (identifier == null || !identifier.contains(".")) {
|
||||
return identifier;
|
||||
}
|
||||
int lastDot = identifier.lastIndexOf('.');
|
||||
int prevDot = identifier.lastIndexOf('.', lastDot - 1);
|
||||
return prevDot > 0 ? identifier.substring(prevDot + 1) : identifier;
|
||||
}
|
||||
|
||||
private static void writeSampleConfig(Path tempDir) throws IOException {
|
||||
Path orderPkg = tempDir.resolve("com/example/order");
|
||||
Path configPkg = tempDir.resolve("com/example/config");
|
||||
|
||||
Reference in New Issue
Block a user