Harden enum identifier validation for rawName sync, polymorphic events, and matched transitions.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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