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