better enricher

This commit is contained in:
2026-06-19 20:56:14 +02:00
parent 344e295106
commit 7077214c81
5 changed files with 136 additions and 6 deletions

View File

@@ -45,6 +45,17 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
boolean isWildcard = triggerEvent.equals("event") || triggerEvent.equals("e") || boolean isWildcard = triggerEvent.equals("event") || triggerEvent.equals("e") ||
triggerEvent.equals("msg") || triggerEvent.equals("message") || triggerEvent.equals("msg") || triggerEvent.equals("message") ||
triggerEvent.equals("payload") || triggerEvent.matches(".*\\.get[A-Z].*\\(\\)"); triggerEvent.equals("payload") || triggerEvent.matches(".*\\.get[A-Z].*\\(\\)");
if (isWildcard) {
String targetVar = chain.getContextMachineId();
if (targetVar == null && chain.getTriggerPoint() != null) {
targetVar = chain.getTriggerPoint().getStateMachineId();
}
if (targetVar == null || targetVar.isEmpty()) {
// Wildcard without specific routing context is too broad, do not link
continue;
}
}
if (isWildcard || smEvent.equals(triggerEvent) || triggerEvent.toLowerCase().contains(smEvent.toLowerCase()) || smEvent.toLowerCase().contains(triggerEvent.toLowerCase())) { if (isWildcard || smEvent.equals(triggerEvent) || triggerEvent.toLowerCase().contains(smEvent.toLowerCase()) || smEvent.toLowerCase().contains(triggerEvent.toLowerCase())) {
// Event matches or is a wildcard // Event matches or is a wildcard
@@ -52,17 +63,27 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName(); String smSourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
String smSource = simplify(smSourceRaw); String smSource = simplify(smSourceRaw);
if (triggerSource == null || triggerSource.equals(smSource)) { if (triggerSource == null || triggerSource.equals(smSource)) {
for (State smTargetState : t.getTargetStates()) { if (t.getTargetStates() == null || t.getTargetStates().isEmpty()) {
String sourceRaw = smSourceState.fullIdentifier() != null ? smSourceState.fullIdentifier() : smSourceState.rawName();
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
MatchedTransition mt = MatchedTransition.builder() MatchedTransition mt = MatchedTransition.builder()
.sourceState(sourceRaw) .sourceState(smSourceRaw)
.targetState(targetRaw) .targetState(smSourceRaw)
.event(smEventRaw) .event(smEventRaw)
.build(); .build();
if (isRoutedToCorrectMachine(chain, result.getName())) { if (isRoutedToCorrectMachine(chain, result.getName())) {
matched.add(mt); matched.add(mt);
} }
} else {
for (State smTargetState : t.getTargetStates()) {
String targetRaw = smTargetState.fullIdentifier() != null ? smTargetState.fullIdentifier() : smTargetState.rawName();
MatchedTransition mt = MatchedTransition.builder()
.sourceState(smSourceRaw)
.targetState(targetRaw)
.event(smEventRaw)
.build();
if (isRoutedToCorrectMachine(chain, result.getName())) {
matched.add(mt);
}
}
} }
} }
} }
@@ -115,6 +136,9 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
if (name == null) return null; if (name == null) return null;
// Strip common suffixes // Strip common suffixes
String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1"); String simplified = name.replaceAll("(?i)(.*)(Event|Action|Transition|Command)(s)?$", "$1");
if (simplified.isEmpty()) {
simplified = name;
}
// Simplify full identifiers to just the last part (enum name) // Simplify full identifiers to just the last part (enum name)
simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1"); simplified = simplified.replaceAll("^.*\\.([A-Z0-9_]+)$", "$1");
// For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify // For remaining full caps with underscores (like EVENT_X), keep as is or try to simplify

View File

@@ -302,7 +302,8 @@ public class GenericEventDetector {
} else if (current.arguments().isEmpty() && current.getExpression() instanceof SimpleName sn) { } else if (current.arguments().isEmpty() && current.getExpression() instanceof SimpleName sn) {
// If the event is obtained by calling a method on a provider/supplier parameter, // If the event is obtained by calling a method on a provider/supplier parameter,
// return the provider's name so CallGraphBuilder can trace the lambda argument // return the provider's name so CallGraphBuilder can trace the lambda argument
return sn.getIdentifier(); String traced = extractEventFromMessageBuilder(sn);
return traced != null ? traced : sn.getIdentifier();
} }
Expression receiver = current.getExpression(); Expression receiver = current.getExpression();

View File

@@ -128,6 +128,7 @@ class TransitionLinkerEnricherTest {
CallChain chain = CallChain.builder() CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("event").build()) .triggerPoint(TriggerPoint.builder().event("event").build())
.contextMachineId("testMachine")
.build(); .build();
AnalysisResult result = AnalysisResult.builder() AnalysisResult result = AnalysisResult.builder()
@@ -151,6 +152,7 @@ class TransitionLinkerEnricherTest {
CallChain chain = CallChain.builder() CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("richEvent.getType()").build()) .triggerPoint(TriggerPoint.builder().event("richEvent.getType()").build())
.contextMachineId("testMachine")
.build(); .build();
AnalysisResult result = AnalysisResult.builder() AnalysisResult result = AnalysisResult.builder()
@@ -164,4 +166,28 @@ class TransitionLinkerEnricherTest {
CallChain updatedChain = result.getMetadata().getCallChains().get(0); CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).hasSize(1); assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
} }
@Test
void shouldIgnoreWildcardWhenContextMachineIdIsMissing() {
Transition t1 = new Transition();
t1.setSourceStates(List.of(State.of("NEW", "NEW")));
t1.setTargetStates(List.of(State.of("PAID", "PAID")));
t1.setEvent(Event.of("PAY", "PAY"));
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder().event("event").build())
// Intentionally NO contextMachineId or stateMachineId
.build();
AnalysisResult result = AnalysisResult.builder()
.name("testMachine")
.transitions(List.of(t1))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
enricher.enrich(result, null, null);
CallChain updatedChain = result.getMetadata().getCallChains().get(0);
assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
}
} }

View File

@@ -115,4 +115,54 @@ class CallGraphBuilderTest {
// Since it's tracing constant concatenation: "machine:" + orderId, which might resolve dynamically or string format. // Since it's tracing constant concatenation: "machine:" + orderId, which might resolve dynamically or string format.
// It should at least be present (it typically extracts 'machine:' + orderId into expression). // It should at least be present (it typically extracts 'machine:' + orderId into expression).
} }
@Test
void shouldResolveTriggerPointParametersAcrossChain() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void createOrder() {
service.updateOrderState(OrderEvents.CREATE);
}
}
class OrderService {
private StateMachine sm;
public void updateOrderState(OrderEvents event) {
sm.sendEvent(event);
}
}
class StateMachine {
public void sendEvent(Object event) {}
}
enum OrderEvents { CREATE, PAY }
""";
Path tempDir = Files.createTempDirectory("callgraph_test");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
CallGraphBuilder builder = new CallGraphBuilder(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("createOrder")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CREATE");
}
} }

View File

@@ -45,6 +45,35 @@ public class GenericEventDetectorTest {
assertThat(triggers.get(0).getMethodName()).isEqualTo("a"); assertThat(triggers.get(0).getMethodName()).isEqualTo("a");
} }
@Test
void testMessageBuilderIntermediateVariable(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example");
Files.createDirectories(dir);
Files.writeString(dir.resolve("MyService.java"),
"package com.example;\n" +
"import org.springframework.messaging.support.MessageBuilder;\n" +
"import org.springframework.messaging.Message;\n" +
"import org.springframework.statemachine.StateMachine;\n" +
"public class MyService {\n" +
" private StateMachine<String, String> stateMachine;\n" +
" private void a(MyInterface myEvent) {\n" +
" MessageBuilder<MyInterface> messageBuilder = MessageBuilder.withPayload(myEvent).setHeader(\"k\", \"v\");\n" +
" stateMachine.sendEvent(messageBuilder.build());\n" +
" }\n" +
"}\n");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, new ConstantResolver(), List.of());
CompilationUnit cu = context.getCompilationUnits().iterator().next();
List<TriggerPoint> triggers = detector.detect(cu);
assertThat(triggers).hasSize(1);
assertThat(triggers.get(0).getEvent()).isEqualTo("myEvent");
assertThat(triggers.get(0).getMethodName()).isEqualTo("a");
}
@Test @Test
void testEnumGetterUnionExtraction(@TempDir Path tempDir) throws IOException { void testEnumGetterUnionExtraction(@TempDir Path tempDir) throws IOException {
Path dir = tempDir.resolve("com/example"); Path dir = tempDir.resolve("com/example");