This commit is contained in:
2026-07-17 07:35:58 +02:00
parent 0c152b0318
commit 40bcde26a3
10 changed files with 491 additions and 140 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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");
}
}

View File

@@ -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 = """