Harden enum identifier validation for rawName sync, polymorphic events, and matched transitions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 12:22:39 +02:00
parent 393c998b66
commit 7ffd157105
5 changed files with 489 additions and 18 deletions

View File

@@ -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;

View File

@@ -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;