A
This commit is contained in:
@@ -303,6 +303,19 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
|
|||||||
stripped = stripped.replaceAll(
|
stripped = stripped.replaceAll(
|
||||||
eventLike + "\\s*\\.\\s*equals\\s*\\(\\s*\"[^\"]+\"\\s*\\)",
|
eventLike + "\\s*\\.\\s*equals\\s*\\(\\s*\"[^\"]+\"\\s*\\)",
|
||||||
"true");
|
"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*&&+", "&&");
|
||||||
stripped = stripped.replaceAll("^\\s*&&\\s*", "");
|
stripped = stripped.replaceAll("^\\s*&&\\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(".")) {
|
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 enumTypeFqn + "." + stripped;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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) {
|
private static String stripQuotes(String value) {
|
||||||
if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
|
if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
|
||||||
return value.substring(1, value.length() - 1);
|
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.EntryPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
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.ConstantResolver;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -137,15 +138,30 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
foundAny = true;
|
foundAny = true;
|
||||||
Map<String, String> pathBindings = resolvePathBindings(path, callGraph, initialBindings);
|
Map<String, String> pathBindings = resolvePathBindings(path, callGraph, initialBindings);
|
||||||
TriggerPoint resolvedTp = resolveTriggerPointParameters(tp, 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) {
|
if (resolvedTp != null) {
|
||||||
resolvedTp = TriggerMachineTypeRebinder.rebind(resolvedTp, path, context);
|
resolvedTp = TriggerMachineTypeRebinder.rebind(resolvedTp, path, context);
|
||||||
String contextMachineId = pathFinder.extractContextMachineId(path, callGraph);
|
String contextMachineId = pathFinder.extractContextMachineId(path, callGraph);
|
||||||
|
Map<String, String> exportedBindings = !pathBindings.isEmpty()
|
||||||
|
? pathBindings
|
||||||
|
: (initialBindings == null || initialBindings.isEmpty()
|
||||||
|
? Map.of()
|
||||||
|
: initialBindings);
|
||||||
chains.add(CallChain.builder()
|
chains.add(CallChain.builder()
|
||||||
.entryPoint(chainEntryPoint)
|
.entryPoint(chainEntryPoint)
|
||||||
.triggerPoint(resolvedTp)
|
.triggerPoint(resolvedTp)
|
||||||
.methodChain(path)
|
.methodChain(path)
|
||||||
.contextMachineId(contextMachineId)
|
.contextMachineId(contextMachineId)
|
||||||
.pathBindings(pathBindings.isEmpty() ? null : Map.copyOf(pathBindings))
|
.pathBindings(exportedBindings.isEmpty() ? null : Map.copyOf(exportedBindings))
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ public class GenericEventDetector {
|
|||||||
this.typeResolver = new TypeResolver(context);
|
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(
|
private static final Set<String> ROUTING_PARAMETER_NAMES = Set.of(
|
||||||
"machineType",
|
"machineType",
|
||||||
"machineId",
|
"machineId",
|
||||||
@@ -37,7 +40,17 @@ public class GenericEventDetector {
|
|||||||
"type",
|
"type",
|
||||||
"action",
|
"action",
|
||||||
"eventString",
|
"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(
|
private static final Set<String> TRIGGER_METHOD_NAMES = Set.of(
|
||||||
"sendEvent", "sendEvents", "sendEventCollect", "sendEventMono", "fire", "trigger");
|
"sendEvent", "sendEvents", "sendEventCollect", "sendEventMono", "fire", "trigger");
|
||||||
@@ -400,6 +413,11 @@ public class GenericEventDetector {
|
|||||||
Expression selector = resolveSwitchSelector(switchCase);
|
Expression selector = resolveSwitchSelector(switchCase);
|
||||||
if (selector != null && !isRoutingParameter(selector) && !switchCase.expressions().isEmpty()) {
|
if (selector != null && !isRoutingParameter(selector) && !switchCase.expressions().isEmpty()) {
|
||||||
Expression caseExpr = (Expression) switchCase.expressions().get(0);
|
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);
|
String resolved = resolveProvableStateLiteral(caseExpr);
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
return resolved;
|
return resolved;
|
||||||
@@ -668,7 +686,12 @@ public class GenericEventDetector {
|
|||||||
|
|
||||||
if (stmtObj instanceof SwitchCase switchCase) {
|
if (stmtObj instanceof SwitchCase switchCase) {
|
||||||
if (!switchCase.expressions().isEmpty()) {
|
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) {
|
} else if (stmtObj instanceof IfStatement ifStmt) {
|
||||||
// Guard clause detection: check if the 'then' block has a return/throw
|
// Guard clause detection: check if the 'then' block has a return/throw
|
||||||
|
|||||||
@@ -196,6 +196,40 @@ class GenericEventDetectorControlFlowTest {
|
|||||||
assertThat(triggers.get(0).getSourceState()).isNull();
|
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
|
@Test
|
||||||
void shouldDetectSourceStateFromLiteralSendEventArgument(@TempDir Path tempDir) throws IOException {
|
void shouldDetectSourceStateFromLiteralSendEventArgument(@TempDir Path tempDir) throws IOException {
|
||||||
String source = """
|
String source = """
|
||||||
|
|||||||
Reference in New Issue
Block a user