A
This commit is contained in:
@@ -6,6 +6,7 @@ import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.EventCarrierPolicy;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
import org.eclipse.jdt.core.dom.ASTVisitor;
|
||||
@@ -161,14 +162,7 @@ public final class CallChainPolyNarrower {
|
||||
if (value == null || value.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
boolean eventLikeKey = key != null && (
|
||||
key.equalsIgnoreCase("event")
|
||||
|| key.equalsIgnoreCase("eventString")
|
||||
|| key.equalsIgnoreCase("command")
|
||||
|| key.equalsIgnoreCase("commandKey")
|
||||
|| key.equalsIgnoreCase("action")
|
||||
|| key.equalsIgnoreCase("actionKey")
|
||||
|| key.toLowerCase(Locale.ROOT).endsWith("event"));
|
||||
boolean eventLikeKey = EventCarrierPolicy.isCarrierParamName(key);
|
||||
int weight = eventLikeKey ? STRONG : MEDIUM;
|
||||
addEnumReference(value, configuredConstants, votes, weight);
|
||||
addPathToken(value, configuredConstants, votes, weight);
|
||||
@@ -186,11 +180,10 @@ public final class CallChainPolyNarrower {
|
||||
while (quoted.find()) {
|
||||
String literal = quoted.group(1);
|
||||
String param = quoted.group(2);
|
||||
if ("event".equals(param) || "eventString".equals(param) || "actionKey".equals(param)
|
||||
|| "action".equals(param)) {
|
||||
if (EventCarrierPolicy.isCarrierParamName(param)) {
|
||||
addPathToken(literal, configuredConstants, votes, MEDIUM);
|
||||
}
|
||||
if ("commandKey".equals(param) || literal.contains(".")) {
|
||||
if (literal.contains(".")) {
|
||||
addPathToken(literal, configuredConstants, votes, MEDIUM);
|
||||
}
|
||||
}
|
||||
@@ -207,8 +200,7 @@ public final class CallChainPolyNarrower {
|
||||
while (reverseEquals.find()) {
|
||||
String param = reverseEquals.group(1);
|
||||
String literal = reverseEquals.group(2);
|
||||
if ("event".equals(param) || "eventString".equals(param) || "actionKey".equals(param)
|
||||
|| "action".equals(param) || "commandKey".equals(param)) {
|
||||
if (EventCarrierPolicy.isCarrierParamName(param)) {
|
||||
addPathToken(literal, configuredConstants, votes, MEDIUM);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.CodebaseIntelligenceProvider;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.EventCarrierPolicy;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.JsonExportContextFactory;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.TriggerMachineTypeRebinder;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.model.State;
|
||||
import click.kamil.springstatemachineexporter.model.Transition;
|
||||
@@ -29,7 +31,6 @@ import click.kamil.springstatemachineexporter.analysis.enricher.routing.SharedSe
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.TriggerMachineTypeRebinder;
|
||||
|
||||
public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
|
||||
@@ -289,8 +290,7 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
}
|
||||
// Event/payload binding clauses prove which event was selected — not which machine.
|
||||
// Keep machineType/domain comparisons for isCompatibleWithMachineDomain.
|
||||
String eventLike =
|
||||
"(?i)(event|e|message|msg|payload|body|command|commandKey|commandkey|action|text)";
|
||||
String eventLike = "(?i)(" + EventCarrierPolicy.carrierNameAlternation() + ")";
|
||||
String stripped = constraint.replaceAll(
|
||||
"\"[^\"]+\"\\s*\\.\\s*equalsIgnoreCase\\s*\\(\\s*" + eventLike + "\\s*\\)",
|
||||
"true");
|
||||
|
||||
@@ -75,6 +75,15 @@ public final class BooleanConstraintEvaluator {
|
||||
return isCompatibleWithBindings(constraint, bindings, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #isCompatibleWithBindings(String, Map)} but unbound equality variables are ignored
|
||||
* instead of failing closed. Used when filtering call chains against entry path/payload bindings
|
||||
* while trigger constraints may also mention unrelated locals.
|
||||
*/
|
||||
public static boolean isCompatibleWithKnownBindings(String constraint, Map<String, String> bindings) {
|
||||
return isCompatibleWithKnownBindings(constraint, bindings, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #isCompatibleWithBindings(String, Map)} but reconstructs truncated enum refs
|
||||
* using {@code preferredEnumTypeFqn} before treating them as concrete.
|
||||
@@ -84,6 +93,23 @@ public final class BooleanConstraintEvaluator {
|
||||
Map<String, String> bindings,
|
||||
String preferredEnumTypeFqn,
|
||||
CodebaseContext context) {
|
||||
return isCompatibleWithBindings(constraint, bindings, preferredEnumTypeFqn, context, false);
|
||||
}
|
||||
|
||||
public static boolean isCompatibleWithKnownBindings(
|
||||
String constraint,
|
||||
Map<String, String> bindings,
|
||||
String preferredEnumTypeFqn,
|
||||
CodebaseContext context) {
|
||||
return isCompatibleWithBindings(constraint, bindings, preferredEnumTypeFqn, context, true);
|
||||
}
|
||||
|
||||
private static boolean isCompatibleWithBindings(
|
||||
String constraint,
|
||||
Map<String, String> bindings,
|
||||
String preferredEnumTypeFqn,
|
||||
CodebaseContext context,
|
||||
boolean ignoreUnboundEqualityVars) {
|
||||
if (constraint == null || constraint.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
@@ -93,6 +119,9 @@ public final class BooleanConstraintEvaluator {
|
||||
Set<String> equalityVars = extractEqualityVariables(constraint);
|
||||
for (String varName : equalityVars) {
|
||||
if (!bindings.containsKey(varName)) {
|
||||
if (ignoreUnboundEqualityVars) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
&& resolvedTp.getConstraint() != null
|
||||
&& constraintBindings != null
|
||||
&& !constraintBindings.isEmpty()
|
||||
&& !BooleanConstraintEvaluator.isCompatibleWithBindings(
|
||||
&& !BooleanConstraintEvaluator.isCompatibleWithKnownBindings(
|
||||
resolvedTp.getConstraint(), constraintBindings)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public final class EntryPointBindingExpander {
|
||||
continue;
|
||||
}
|
||||
if (!isRequestParam(parameter)
|
||||
&& !isMessagingPayloadParameter(entryPoint, parameter)
|
||||
&& !EventCarrierPolicy.isMessagingPayloadParameter(entryPoint, parameter)
|
||||
&& isPlaceholderResolvedInPath(entryPoint, parameter.getName())) {
|
||||
continue;
|
||||
}
|
||||
@@ -284,7 +284,8 @@ public final class EntryPointBindingExpander {
|
||||
return false;
|
||||
}
|
||||
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
||||
if (paramName.equals(parameter.getName()) && isMessagingPayloadParameter(entryPoint, parameter)) {
|
||||
if (paramName.equals(parameter.getName())
|
||||
&& EventCarrierPolicy.isMessagingPayloadParameter(entryPoint, parameter)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -292,7 +293,8 @@ public final class EntryPointBindingExpander {
|
||||
}
|
||||
|
||||
private static boolean isExpandableBindingParameter(EntryPoint entryPoint, EntryPoint.Parameter parameter) {
|
||||
return isPathOrQueryVariable(parameter) || isMessagingPayloadParameter(entryPoint, parameter);
|
||||
return isPathOrQueryVariable(parameter)
|
||||
|| EventCarrierPolicy.isMessagingPayloadParameter(entryPoint, parameter);
|
||||
}
|
||||
|
||||
private static boolean isPathOrQueryVariable(EntryPoint.Parameter parameter) {
|
||||
@@ -303,60 +305,6 @@ public final class EntryPointBindingExpander {
|
||||
return hasSimpleAnnotation(parameter, "RequestParam");
|
||||
}
|
||||
|
||||
/**
|
||||
* Messaging listeners carry the event key in {@code @Payload} or an unannotated String body
|
||||
* ({@code message}, {@code payload}, {@code event}, …) — same expansion surface as REST path vars.
|
||||
*/
|
||||
private static boolean isMessagingPayloadParameter(EntryPoint entryPoint, EntryPoint.Parameter parameter) {
|
||||
if (entryPoint == null || parameter == null || !isMessagingEntryPoint(entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (hasSimpleAnnotation(parameter, "Payload")) {
|
||||
return true;
|
||||
}
|
||||
boolean unannotated = parameter.getAnnotations() == null || parameter.getAnnotations().isEmpty();
|
||||
if (!unannotated) {
|
||||
return false;
|
||||
}
|
||||
String type = parameter.getType();
|
||||
if (type == null) {
|
||||
return false;
|
||||
}
|
||||
String simpleType = type.contains(".") ? type.substring(type.lastIndexOf('.') + 1) : type;
|
||||
if (!"String".equals(simpleType)) {
|
||||
return false;
|
||||
}
|
||||
return isMessageOrEventParamName(parameter.getName());
|
||||
}
|
||||
|
||||
private static boolean isMessagingEntryPoint(EntryPoint entryPoint) {
|
||||
if (entryPoint.getType() == null) {
|
||||
return false;
|
||||
}
|
||||
return switch (entryPoint.getType()) {
|
||||
case JMS, KAFKA, RABBIT, SQS, SNS -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isMessageOrEventParamName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String lower = name.toLowerCase();
|
||||
return lower.equals("message")
|
||||
|| lower.equals("msg")
|
||||
|| lower.equals("payload")
|
||||
|| lower.equals("body")
|
||||
|| lower.equals("event")
|
||||
|| lower.equals("e")
|
||||
|| lower.endsWith("event")
|
||||
|| lower.equals("command")
|
||||
|| lower.equals("commandkey")
|
||||
|| lower.equals("action")
|
||||
|| lower.equals("text");
|
||||
}
|
||||
|
||||
private static boolean hasSimpleAnnotation(EntryPoint.Parameter parameter, String simpleName) {
|
||||
if (parameter.getAnnotations() == null || simpleName == null) {
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Shared identity for parameters that carry event / payload / machine-route discriminators
|
||||
* rather than SM source-state enum values. Used by binding expansion, trigger detection,
|
||||
* linking, and poly narrowing so name lists are not duplicated.
|
||||
*/
|
||||
public final class EventCarrierPolicy {
|
||||
|
||||
private static final Set<String> CARRIER_NAMES = Set.of(
|
||||
"message",
|
||||
"msg",
|
||||
"payload",
|
||||
"body",
|
||||
"command",
|
||||
"commandkey",
|
||||
"event",
|
||||
"e",
|
||||
"eventstring",
|
||||
"action",
|
||||
"actionkey",
|
||||
"text",
|
||||
"transition");
|
||||
|
||||
private static final Set<String> MACHINE_DISCRIMINATOR_NAMES = Set.of(
|
||||
"machinetype",
|
||||
"machineid",
|
||||
"machinename",
|
||||
"domain",
|
||||
"type",
|
||||
"version");
|
||||
|
||||
private static final Set<String> STATE_ACCESSOR_NAMES = Set.of(
|
||||
"state",
|
||||
"currentstate",
|
||||
"sourcestate",
|
||||
"fromstate");
|
||||
|
||||
private EventCarrierPolicy() {
|
||||
}
|
||||
|
||||
/** Event / payload / command body parameter names (not machine domain keys). */
|
||||
public static boolean isCarrierParamName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String lower = name.toLowerCase(Locale.ROOT);
|
||||
if (CARRIER_NAMES.contains(lower)) {
|
||||
return true;
|
||||
}
|
||||
return lower.endsWith("event");
|
||||
}
|
||||
|
||||
/** Machine-type / domain discriminators used for multi-machine routing. */
|
||||
public static boolean isMachineDiscriminatorName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return MACHINE_DISCRIMINATOR_NAMES.contains(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/** Carrier or machine discriminant — must not be treated as SM source state. */
|
||||
public static boolean isRoutingDiscriminantName(String name) {
|
||||
return isCarrierParamName(name) || isMachineDiscriminatorName(name);
|
||||
}
|
||||
|
||||
/** Clear state-variable / accessor names used for positive source-state inference. */
|
||||
public static boolean isStateAccessorName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String lower = name.toLowerCase(Locale.ROOT);
|
||||
if (STATE_ACCESSOR_NAMES.contains(lower)) {
|
||||
return true;
|
||||
}
|
||||
return lower.endsWith("state") && !isCarrierParamName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Messaging {@code @Payload} or unannotated String body that looks like an event carrier.
|
||||
*/
|
||||
public static boolean isMessagingPayloadParameter(EntryPoint entryPoint, EntryPoint.Parameter parameter) {
|
||||
if (entryPoint == null || parameter == null || !isMessagingEntryPoint(entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (hasAnnotation(parameter, "Payload")) {
|
||||
return true;
|
||||
}
|
||||
boolean unannotated = parameter.getAnnotations() == null || parameter.getAnnotations().isEmpty();
|
||||
if (!unannotated) {
|
||||
return false;
|
||||
}
|
||||
if (!isStringType(parameter.getType())) {
|
||||
return false;
|
||||
}
|
||||
return isCarrierParamName(parameter.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* REST path/query params that carry the event key (not resource ids).
|
||||
*/
|
||||
public static boolean isRestEventCarrierParameter(EntryPoint.Parameter parameter) {
|
||||
if (parameter == null || parameter.getAnnotations() == null) {
|
||||
return false;
|
||||
}
|
||||
boolean pathOrQuery = hasAnnotation(parameter, "PathVariable")
|
||||
|| hasAnnotation(parameter, "RequestParam");
|
||||
if (!pathOrQuery) {
|
||||
return false;
|
||||
}
|
||||
return isCarrierParamName(parameter.getName()) || isMachineDiscriminatorName(parameter.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry-point parameter that expands / binds as an event or payload carrier.
|
||||
*/
|
||||
public static boolean isCarrierParameter(EntryPoint entryPoint, EntryPoint.Parameter parameter) {
|
||||
if (parameter == null) {
|
||||
return false;
|
||||
}
|
||||
if (hasAnnotation(parameter, "Payload")) {
|
||||
return true;
|
||||
}
|
||||
if (isMessagingPayloadParameter(entryPoint, parameter)) {
|
||||
return true;
|
||||
}
|
||||
if (isRestEventCarrierParameter(parameter)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isMessagingEntryPoint(EntryPoint entryPoint) {
|
||||
if (entryPoint == null || entryPoint.getType() == null) {
|
||||
return false;
|
||||
}
|
||||
return switch (entryPoint.getType()) {
|
||||
case JMS, KAFKA, RABBIT, SQS, SNS -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternation for regex consumers (linker constraint stripping). Names are already lowercased
|
||||
* where case-insensitive flags are used by the caller.
|
||||
*/
|
||||
public static String carrierNameAlternation() {
|
||||
return Stream.concat(
|
||||
CARRIER_NAMES.stream(),
|
||||
Stream.of("commandKey"))
|
||||
.distinct()
|
||||
.sorted()
|
||||
.collect(Collectors.joining("|"));
|
||||
}
|
||||
|
||||
public static String routingDiscriminantAlternation() {
|
||||
return Stream.concat(CARRIER_NAMES.stream(), MACHINE_DISCRIMINATOR_NAMES.stream())
|
||||
.sorted()
|
||||
.collect(Collectors.joining("|"));
|
||||
}
|
||||
|
||||
private static boolean hasAnnotation(EntryPoint.Parameter parameter, String simpleName) {
|
||||
if (parameter.getAnnotations() == null || simpleName == null) {
|
||||
return false;
|
||||
}
|
||||
return parameter.getAnnotations().stream().anyMatch(a ->
|
||||
simpleName.equals(a) || (a != null && a.endsWith("." + simpleName)));
|
||||
}
|
||||
|
||||
private static boolean isStringType(String type) {
|
||||
if (type == null) {
|
||||
return false;
|
||||
}
|
||||
String simple = type.contains(".") ? type.substring(type.lastIndexOf('.') + 1) : type;
|
||||
return "String".equals(simple);
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public final class ExternalTriggerPolicy {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (entryPoint != null && isMessagingEntryPoint(entryPoint)) {
|
||||
if (entryPoint != null && EventCarrierPolicy.isMessagingEntryPoint(entryPoint)) {
|
||||
String paramName = resolveMessagingEventParameterName(entryPoint, resolvedEventParamName);
|
||||
if (paramName != null) {
|
||||
RestParamBinding binding = findRestParameterBinding(entryPoint, paramName, context);
|
||||
@@ -125,16 +125,6 @@ public final class ExternalTriggerPolicy {
|
||||
return trigger.isExternal();
|
||||
}
|
||||
|
||||
private static boolean isMessagingEntryPoint(EntryPoint entryPoint) {
|
||||
if (entryPoint == null || entryPoint.getType() == null) {
|
||||
return false;
|
||||
}
|
||||
return switch (entryPoint.getType()) {
|
||||
case JMS, KAFKA, RABBIT, SQS, SNS -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean hasConcreteSinglePoly(TriggerPoint trigger) {
|
||||
if (trigger.getPolymorphicEvents() == null || trigger.getPolymorphicEvents().isEmpty()) {
|
||||
return false;
|
||||
@@ -160,13 +150,7 @@ public final class ExternalTriggerPolicy {
|
||||
}
|
||||
for (EntryPoint.Parameter parameter : entryPoint.getParameters()) {
|
||||
if ((parameter.getAnnotations() == null || parameter.getAnnotations().isEmpty())
|
||||
&& isEventLikeParameterName(parameter.getName())) {
|
||||
return parameter.getName();
|
||||
}
|
||||
String lower = parameter.getName() == null ? "" : parameter.getName().toLowerCase();
|
||||
if ((parameter.getAnnotations() == null || parameter.getAnnotations().isEmpty())
|
||||
&& (lower.equals("message") || lower.equals("msg") || lower.equals("payload")
|
||||
|| lower.equals("body") || lower.equals("text"))) {
|
||||
&& EventCarrierPolicy.isCarrierParamName(parameter.getName())) {
|
||||
return parameter.getName();
|
||||
}
|
||||
}
|
||||
@@ -204,7 +188,8 @@ public final class ExternalTriggerPolicy {
|
||||
return false;
|
||||
}
|
||||
String placeholder = pathOrName.substring(open + 1, close).trim();
|
||||
if (isEventLikeParameterName(placeholderName(placeholder))) {
|
||||
if (EventCarrierPolicy.isCarrierParamName(placeholderName(placeholder))
|
||||
|| EventCarrierPolicy.isMachineDiscriminatorName(placeholderName(placeholder))) {
|
||||
return true;
|
||||
}
|
||||
start = close + 1;
|
||||
@@ -240,7 +225,8 @@ public final class ExternalTriggerPolicy {
|
||||
if (!pathVariable && !requestParam) {
|
||||
continue;
|
||||
}
|
||||
if (isEventLikeParameterName(parameter.getName())) {
|
||||
if (EventCarrierPolicy.isCarrierParamName(parameter.getName())
|
||||
|| EventCarrierPolicy.isMachineDiscriminatorName(parameter.getName())) {
|
||||
return parameter.getName();
|
||||
}
|
||||
}
|
||||
@@ -259,21 +245,6 @@ public final class ExternalTriggerPolicy {
|
||||
&& value.chars().allMatch(Character::isJavaIdentifierPart);
|
||||
}
|
||||
|
||||
private static boolean isEventLikeParameterName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String lower = name.toLowerCase();
|
||||
return lower.equals("event")
|
||||
|| lower.equals("e")
|
||||
|| lower.endsWith("event")
|
||||
|| lower.equals("command")
|
||||
|| lower.equals("commandkey")
|
||||
|| lower.equals("transition")
|
||||
|| lower.equals("action")
|
||||
|| lower.equals("machinetype");
|
||||
}
|
||||
|
||||
private static RestParamBinding findRestParameterBinding(
|
||||
EntryPoint entryPoint, String paramName, CodebaseContext context) {
|
||||
if (entryPoint.getParameters() == null || paramName == null) {
|
||||
|
||||
@@ -28,30 +28,6 @@ public class GenericEventDetector {
|
||||
this.typeResolver = new TypeResolver(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter names that select a route/event payload, not SM source-state enum constants.
|
||||
* Includes messaging body names so {@code switch (message)} case labels are not treated as states.
|
||||
*/
|
||||
private static final Set<String> ROUTING_PARAMETER_NAMES = Set.of(
|
||||
"machineType",
|
||||
"machineId",
|
||||
"machineName",
|
||||
"domain",
|
||||
"type",
|
||||
"action",
|
||||
"eventString",
|
||||
"version",
|
||||
// JMS / Kafka / Rabbit payload carriers (parity with EntryPointBindingExpander)
|
||||
"message",
|
||||
"msg",
|
||||
"payload",
|
||||
"body",
|
||||
"command",
|
||||
"commandKey",
|
||||
"commandkey",
|
||||
"text",
|
||||
"event");
|
||||
|
||||
private static final Set<String> TRIGGER_METHOD_NAMES = Set.of(
|
||||
"sendEvent", "sendEvents", "sendEventCollect", "sendEventMono", "fire", "trigger");
|
||||
|
||||
@@ -400,18 +376,20 @@ public class GenericEventDetector {
|
||||
String state = extractStateFromSiblings(current, block.statements());
|
||||
if (state != null) return state;
|
||||
} else if (parent instanceof SwitchStatement switchStmt) {
|
||||
if (!isRoutingParameter(switchStmt.getExpression())) {
|
||||
if (isSourceStateSelector(switchStmt.getExpression())) {
|
||||
String state = extractStateFromSiblings(current, switchStmt.statements());
|
||||
if (state != null) return state;
|
||||
}
|
||||
} else if (parent instanceof SwitchExpression switchExpr) {
|
||||
if (!isRoutingParameter(switchExpr.getExpression())) {
|
||||
if (isSourceStateSelector(switchExpr.getExpression())) {
|
||||
String state = extractStateFromSiblings(current, switchExpr.statements());
|
||||
if (state != null) return state;
|
||||
}
|
||||
} else if (parent instanceof SwitchCase switchCase && !switchCase.isDefault()) {
|
||||
Expression selector = resolveSwitchSelector(switchCase);
|
||||
if (selector != null && !isRoutingParameter(selector) && !switchCase.expressions().isEmpty()) {
|
||||
if (selector != null
|
||||
&& isSourceStateSelector(selector)
|
||||
&& !switchCase.expressions().isEmpty()) {
|
||||
Expression caseExpr = (Expression) switchCase.expressions().get(0);
|
||||
// String case labels are routing keys (JMS payload / command), not state enums.
|
||||
if (caseExpr instanceof StringLiteral) {
|
||||
@@ -731,7 +709,7 @@ public class GenericEventDetector {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isRoutingParameter(left) || isRoutingParameter(right)) {
|
||||
if (isRoutingDiscriminant(left) || isRoutingDiscriminant(right)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -750,7 +728,7 @@ public class GenericEventDetector {
|
||||
Expression receiver = mi.getExpression();
|
||||
Expression arg = (Expression) mi.arguments().get(0);
|
||||
|
||||
if (isRoutingParameter(receiver) || isRoutingParameter(arg)) {
|
||||
if (isRoutingDiscriminant(receiver) || isRoutingDiscriminant(arg)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -764,13 +742,105 @@ public class GenericEventDetector {
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isRoutingParameter(Expression expr) {
|
||||
/**
|
||||
* Positive evidence that a switch/if selector is an SM source state (not a payload/route key).
|
||||
*/
|
||||
private boolean isSourceStateSelector(Expression expr) {
|
||||
if (expr == null || isRoutingDiscriminant(expr)) {
|
||||
return false;
|
||||
}
|
||||
if (expr instanceof SimpleName sn) {
|
||||
return ROUTING_PARAMETER_NAMES.contains(sn.getIdentifier());
|
||||
if (EventCarrierPolicy.isStateAccessorName(sn.getIdentifier())) {
|
||||
return true;
|
||||
}
|
||||
if (isCarrierStringParameter(sn)) {
|
||||
return false;
|
||||
}
|
||||
return isLikelyStateEnumType(sn);
|
||||
}
|
||||
if (expr instanceof MethodInvocation mi) {
|
||||
String name = mi.getName().getIdentifier();
|
||||
if ("getState".equals(name) || "getId".equals(name) || EventCarrierPolicy.isStateAccessorName(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return isLikelyStateEnumType(expr);
|
||||
}
|
||||
|
||||
private boolean isRoutingDiscriminant(Expression expr) {
|
||||
if (expr instanceof SimpleName sn) {
|
||||
if (EventCarrierPolicy.isRoutingDiscriminantName(sn.getIdentifier())) {
|
||||
return true;
|
||||
}
|
||||
return isCarrierStringParameter(sn);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code String} method parameter that carries a payload/event key (by name, {@code @Payload},
|
||||
* or messaging body convention) — never a source-state selector.
|
||||
*/
|
||||
private boolean isCarrierStringParameter(SimpleName name) {
|
||||
MethodDeclaration method = click.kamil.springstatemachineexporter.ast.common.AstUtils
|
||||
.findEnclosingMethod(name);
|
||||
if (method == null) {
|
||||
return false;
|
||||
}
|
||||
for (Object paramObj : method.parameters()) {
|
||||
if (!(paramObj instanceof SingleVariableDeclaration param)) {
|
||||
continue;
|
||||
}
|
||||
if (!param.getName().getIdentifier().equals(name.getIdentifier())) {
|
||||
continue;
|
||||
}
|
||||
if (!isStringType(param.getType())) {
|
||||
return false;
|
||||
}
|
||||
if (hasPayloadAnnotation(param)) {
|
||||
return true;
|
||||
}
|
||||
return EventCarrierPolicy.isCarrierParamName(param.getName().getIdentifier());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isStringType(org.eclipse.jdt.core.dom.Type type) {
|
||||
if (type == null) {
|
||||
return false;
|
||||
}
|
||||
String raw = type.toString();
|
||||
String simple = raw.contains(".") ? raw.substring(raw.lastIndexOf('.') + 1) : raw;
|
||||
return "String".equals(simple);
|
||||
}
|
||||
|
||||
private static boolean hasPayloadAnnotation(SingleVariableDeclaration param) {
|
||||
for (Object modifier : param.modifiers()) {
|
||||
if (modifier instanceof Annotation annotation) {
|
||||
String typeName = annotation.getTypeName().getFullyQualifiedName();
|
||||
if ("Payload".equals(typeName) || typeName.endsWith(".Payload")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isLikelyStateEnumType(Expression expr) {
|
||||
if (expr == null) {
|
||||
return false;
|
||||
}
|
||||
ITypeBinding binding = expr.resolveTypeBinding();
|
||||
if (binding == null) {
|
||||
return false;
|
||||
}
|
||||
if (!binding.isEnum()) {
|
||||
return false;
|
||||
}
|
||||
String simple = binding.getName();
|
||||
return simple != null && simple.toLowerCase(java.util.Locale.ROOT).contains("state");
|
||||
}
|
||||
|
||||
private String getSimpleNameString(Expression expr) {
|
||||
if (expr instanceof QualifiedName qn) {
|
||||
return qn.getName().getIdentifier();
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class EventCarrierPolicyTest {
|
||||
|
||||
@Test
|
||||
void carrierNamesIncludeMessagingAndEventConventions() {
|
||||
assertThat(EventCarrierPolicy.isCarrierParamName("message")).isTrue();
|
||||
assertThat(EventCarrierPolicy.isCarrierParamName("payload")).isTrue();
|
||||
assertThat(EventCarrierPolicy.isCarrierParamName("commandKey")).isTrue();
|
||||
assertThat(EventCarrierPolicy.isCarrierParamName("orderEvent")).isTrue();
|
||||
assertThat(EventCarrierPolicy.isCarrierParamName("orderId")).isFalse();
|
||||
assertThat(EventCarrierPolicy.isCarrierParamName("cmd")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void machineDiscriminatorsAreRoutingButNotCarriers() {
|
||||
assertThat(EventCarrierPolicy.isMachineDiscriminatorName("machineType")).isTrue();
|
||||
assertThat(EventCarrierPolicy.isMachineDiscriminatorName("domain")).isTrue();
|
||||
assertThat(EventCarrierPolicy.isCarrierParamName("machineType")).isFalse();
|
||||
assertThat(EventCarrierPolicy.isRoutingDiscriminantName("machineType")).isTrue();
|
||||
assertThat(EventCarrierPolicy.isRoutingDiscriminantName("message")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void stateAccessorNamesArePositiveEvidence() {
|
||||
assertThat(EventCarrierPolicy.isStateAccessorName("state")).isTrue();
|
||||
assertThat(EventCarrierPolicy.isStateAccessorName("currentState")).isTrue();
|
||||
assertThat(EventCarrierPolicy.isStateAccessorName("message")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void payloadAnnotationMakesMessagingCarrierRegardlessOfName() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.type(EntryPoint.Type.JMS)
|
||||
.name("JMS: q")
|
||||
.className("com.example.L")
|
||||
.methodName("onMessage")
|
||||
.parameters(List.of(EntryPoint.Parameter.builder()
|
||||
.name("cmd")
|
||||
.type("String")
|
||||
.annotations(List.of("Payload"))
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
assertThat(EventCarrierPolicy.isMessagingPayloadParameter(
|
||||
entryPoint, entryPoint.getParameters().get(0))).isTrue();
|
||||
assertThat(EventCarrierPolicy.isCarrierParameter(
|
||||
entryPoint, entryPoint.getParameters().get(0))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unannotatedMessagingStringUsesCarrierNameConvention() {
|
||||
EntryPoint entryPoint = EntryPoint.builder()
|
||||
.type(EntryPoint.Type.JMS)
|
||||
.name("JMS: q")
|
||||
.className("com.example.L")
|
||||
.methodName("onMessage")
|
||||
.parameters(List.of(EntryPoint.Parameter.builder()
|
||||
.name("message")
|
||||
.type("String")
|
||||
.annotations(List.of())
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
assertThat(EventCarrierPolicy.isMessagingPayloadParameter(
|
||||
entryPoint, entryPoint.getParameters().get(0))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void carrierNameAlternationIsNonEmptyForLinkerRegex() {
|
||||
assertThat(EventCarrierPolicy.carrierNameAlternation())
|
||||
.contains("message")
|
||||
.contains("payload")
|
||||
.contains("event");
|
||||
}
|
||||
}
|
||||
@@ -230,6 +230,80 @@ class GenericEventDetectorControlFlowTest {
|
||||
.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotTreatPayloadAnnotatedCmdSwitchAsSourceState(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
public class OrderListener {
|
||||
private StateMachine sm;
|
||||
public void onMessage(@Payload String cmd) {
|
||||
switch (cmd) {
|
||||
case "PAY" -> sm.sendEvent(OrderEvent.PAY);
|
||||
case "SHIP" -> sm.sendEvent(OrderEvent.SHIP);
|
||||
}
|
||||
}
|
||||
}
|
||||
class StateMachine {
|
||||
public void sendEvent(OrderEvent e) {}
|
||||
}
|
||||
enum OrderState { NEW, PAID, SHIPPED }
|
||||
enum OrderEvent { PAY, SHIP }
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("OrderListener.java"), source);
|
||||
Files.writeString(tempDir.resolve("Payload.java"), """
|
||||
package org.springframework.messaging.handler.annotation;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Payload {}
|
||||
""");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
|
||||
List<TriggerPoint> triggers = detector.detect(
|
||||
(org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderListener").getRoot());
|
||||
|
||||
assertThat(triggers).hasSize(2);
|
||||
assertThat(triggers).allSatisfy(t -> assertThat(t.getSourceState())
|
||||
.as("@Payload String cmd switch must not invent sourceState")
|
||||
.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotTreatNonCarrierStringSwitchAsSourceState(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderListener {
|
||||
private StateMachine sm;
|
||||
public void onMessage(String cmd) {
|
||||
switch (cmd) {
|
||||
case "PAY" -> sm.sendEvent(OrderEvent.PAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
class StateMachine {
|
||||
public void sendEvent(OrderEvent e) {}
|
||||
}
|
||||
enum OrderEvent { PAY }
|
||||
""";
|
||||
Files.writeString(tempDir.resolve("OrderListener.java"), source);
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), Collections.emptyList());
|
||||
List<TriggerPoint> triggers = detector.detect(
|
||||
(org.eclipse.jdt.core.dom.CompilationUnit) context.getTypeDeclaration("com.example.OrderListener").getRoot());
|
||||
|
||||
assertThat(triggers).hasSize(1);
|
||||
assertThat(triggers.get(0).getSourceState())
|
||||
.as("String switch selector that is not a state enum must not become sourceState")
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDetectSourceStateFromLiteralSendEventArgument(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
|
||||
Reference in New Issue
Block a user