A
This commit is contained in:
@@ -303,6 +303,19 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
||||
stripped = stripped.replaceAll(
|
||||
eventLike + "\\s*\\.\\s*equals\\s*\\(\\s*\"[^\"]+\"\\s*\\)",
|
||||
"true");
|
||||
// Switch-case / arrow constraints often use == on the payload param.
|
||||
stripped = stripped.replaceAll(
|
||||
eventLike + "\\s*==\\s*\"[^\"]+\"",
|
||||
"true");
|
||||
stripped = stripped.replaceAll(
|
||||
"\"[^\"]+\"\\s*==\\s*" + eventLike,
|
||||
"true");
|
||||
stripped = stripped.replaceAll(
|
||||
eventLike + "\\s*!=\\s*\"[^\"]+\"",
|
||||
"true");
|
||||
stripped = stripped.replaceAll(
|
||||
"\"[^\"]+\"\\s*!=\\s*" + eventLike,
|
||||
"true");
|
||||
stripped = stripped.replaceAll("&&\\s*&&+", "&&");
|
||||
stripped = stripped.replaceAll("^\\s*&&\\s*", "");
|
||||
stripped = stripped.replaceAll("\\s*&&\\s*$", "");
|
||||
|
||||
@@ -928,12 +928,36 @@ public final class MachineEnumCanonicalizer {
|
||||
}
|
||||
|
||||
if (typePart == null && Character.isUpperCase(stripped.charAt(0)) && !stripped.contains(".")) {
|
||||
// Only invent enumType.CONST when CONST is a real member — otherwise JMS payload
|
||||
// labels like "PAY" become OrderState.PAY and break transition linking.
|
||||
if (context != null && !enumHasConstant(enumTypeFqn, stripped, context)) {
|
||||
return stripped;
|
||||
}
|
||||
return enumTypeFqn + "." + stripped;
|
||||
}
|
||||
|
||||
return stripped;
|
||||
}
|
||||
|
||||
private static boolean enumHasConstant(String enumTypeFqn, String constant, CodebaseContext context) {
|
||||
if (enumTypeFqn == null || constant == null || context == null) {
|
||||
return false;
|
||||
}
|
||||
List<String> values = context.getEnumValues(stripGenerics(enumTypeFqn));
|
||||
if (values == null || values.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
if (value.equals(constant) || value.endsWith("." + constant)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String stripQuotes(String value) {
|
||||
if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
|
||||
return value.substring(1, value.length() - 1);
|
||||
|
||||
@@ -12,6 +12,7 @@ import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.BooleanConstraintEvaluator;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -137,15 +138,30 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
||||
foundAny = true;
|
||||
Map<String, String> pathBindings = resolvePathBindings(path, callGraph, initialBindings);
|
||||
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, path, callGraph, initialBindings);
|
||||
Map<String, String> constraintBindings =
|
||||
!pathBindings.isEmpty() ? pathBindings : initialBindings;
|
||||
if (resolvedTp != null
|
||||
&& resolvedTp.getConstraint() != null
|
||||
&& constraintBindings != null
|
||||
&& !constraintBindings.isEmpty()
|
||||
&& !BooleanConstraintEvaluator.isCompatibleWithBindings(
|
||||
resolvedTp.getConstraint(), constraintBindings)) {
|
||||
continue;
|
||||
}
|
||||
if (resolvedTp != null) {
|
||||
resolvedTp = TriggerMachineTypeRebinder.rebind(resolvedTp, path, context);
|
||||
String contextMachineId = pathFinder.extractContextMachineId(path, callGraph);
|
||||
Map<String, String> exportedBindings = !pathBindings.isEmpty()
|
||||
? pathBindings
|
||||
: (initialBindings == null || initialBindings.isEmpty()
|
||||
? Map.of()
|
||||
: initialBindings);
|
||||
chains.add(CallChain.builder()
|
||||
.entryPoint(chainEntryPoint)
|
||||
.triggerPoint(resolvedTp)
|
||||
.methodChain(path)
|
||||
.contextMachineId(contextMachineId)
|
||||
.pathBindings(pathBindings.isEmpty() ? null : Map.copyOf(pathBindings))
|
||||
.pathBindings(exportedBindings.isEmpty() ? null : Map.copyOf(exportedBindings))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ public class GenericEventDetector {
|
||||
this.typeResolver = new TypeResolver(context);
|
||||
}
|
||||
|
||||
/** Parameter names that route to a machine, not SM state enum constants. */
|
||||
/**
|
||||
* 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",
|
||||
@@ -37,7 +40,17 @@ public class GenericEventDetector {
|
||||
"type",
|
||||
"action",
|
||||
"eventString",
|
||||
"version");
|
||||
"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,6 +413,11 @@ public class GenericEventDetector {
|
||||
Expression selector = resolveSwitchSelector(switchCase);
|
||||
if (selector != null && !isRoutingParameter(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) {
|
||||
current = parent;
|
||||
continue;
|
||||
}
|
||||
String resolved = resolveProvableStateLiteral(caseExpr);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
@@ -668,7 +686,12 @@ public class GenericEventDetector {
|
||||
|
||||
if (stmtObj instanceof SwitchCase switchCase) {
|
||||
if (!switchCase.expressions().isEmpty()) {
|
||||
return getSimpleNameString((Expression) switchCase.expressions().get(0));
|
||||
Expression caseExpr = (Expression) switchCase.expressions().get(0);
|
||||
// String case labels are routing keys, not SM source states.
|
||||
if (caseExpr instanceof StringLiteral) {
|
||||
continue;
|
||||
}
|
||||
return getSimpleNameString(caseExpr);
|
||||
}
|
||||
} else if (stmtObj instanceof IfStatement ifStmt) {
|
||||
// Guard clause detection: check if the 'then' block has a return/throw
|
||||
|
||||
@@ -196,6 +196,40 @@ class GenericEventDetectorControlFlowTest {
|
||||
assertThat(triggers.get(0).getSourceState()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotTreatJmsMessageSwitchCasesAsSourceState(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
package com.example;
|
||||
public class OrderListener {
|
||||
private StateMachine sm;
|
||||
public void onMessage(String message) {
|
||||
switch (message) {
|
||||
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);
|
||||
|
||||
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("JMS payload case labels must not become sourceState")
|
||||
.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDetectSourceStateFromLiteralSendEventArgument(@TempDir Path tempDir) throws IOException {
|
||||
String source = """
|
||||
|
||||
Reference in New Issue
Block a user