Expand entry-point and trigger detector coverage.

Detect fire/trigger wrappers as state-machine triggers, resolve Kafka topicPattern and additional Rabbit listener attributes, and add regression tests for WebFlux RouterFunction routes and messaging listeners.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 05:50:34 +02:00
parent b158179559
commit 23a14d8391
6 changed files with 217 additions and 9 deletions

View File

@@ -31,6 +31,9 @@ public class GenericEventDetector {
"eventString",
"version");
private static final Set<String> TRIGGER_METHOD_NAMES = Set.of(
"sendEvent", "sendEvents", "sendEventCollect", "sendEventMono", "fire", "trigger");
public List<TriggerPoint> detect(CompilationUnit cu) {
List<TriggerPoint> triggers = new ArrayList<>();
String fileName = "unknown";
@@ -45,15 +48,13 @@ public class GenericEventDetector {
public boolean visit(MethodInvocation node) {
String methodName = node.getName().getIdentifier();
if ("sendEvent".equals(methodName) || "sendEvents".equals(methodName) ||
"sendEventCollect".equals(methodName) || "sendEventMono".equals(methodName)) {
if (TRIGGER_METHOD_NAMES.contains(methodName)) {
processSendEvent(node, cu, triggers);
} else {
for (Object argObj : node.arguments()) {
if (argObj instanceof ExpressionMethodReference emr) {
String refMethod = emr.getName().getIdentifier();
if ("sendEvent".equals(refMethod) || "sendEvents".equals(refMethod) ||
"sendEventCollect".equals(refMethod) || "sendEventMono".equals(refMethod)) {
if (TRIGGER_METHOD_NAMES.contains(refMethod)) {
processSendEventMethodReference(node, emr, cu, triggers);
}
}

View File

@@ -40,6 +40,12 @@ public class MessagingDetector {
} else if (typeName.endsWith("RabbitListener")) {
type = EntryPoint.Type.RABBIT;
destination = resolveValue(annotation, "queues");
if ("unknown".equals(destination)) {
destination = resolveValue(annotation, "queue");
}
if ("unknown".equals(destination)) {
destination = resolveValue(annotation, "bindings");
}
} else if (typeName.endsWith("SqsListener")) {
type = EntryPoint.Type.SQS;
destination = resolveValue(annotation, "value");
@@ -55,6 +61,9 @@ public class MessagingDetector {
} else if (typeName.endsWith("KafkaListener")) {
type = EntryPoint.Type.KAFKA;
destination = resolveValue(annotation, "topics");
if ("unknown".equals(destination)) {
destination = resolveValue(annotation, "topicPattern");
}
}
if (type != null) {

View File

@@ -182,11 +182,11 @@ public class SpringMvcDetector {
}
private String getHttpVerb(String annotationName) {
if (annotationName.endsWith("PostMapping") || annotationName.endsWith("POST")) return "POST";
if (annotationName.endsWith("GetMapping") || annotationName.endsWith("GET")) return "GET";
if (annotationName.endsWith("PutMapping") || annotationName.endsWith("PUT")) return "PUT";
if (annotationName.endsWith("DeleteMapping") || annotationName.endsWith("DELETE")) return "DELETE";
if (annotationName.endsWith("PatchMapping") || annotationName.endsWith("PATCH")) return "PATCH";
if (annotationName.endsWith("PostMapping") || annotationName.endsWith("POST") || "POST".equals(annotationName)) return "POST";
if (annotationName.endsWith("GetMapping") || annotationName.endsWith("GET") || "GET".equals(annotationName)) return "GET";
if (annotationName.endsWith("PutMapping") || annotationName.endsWith("PUT") || "PUT".equals(annotationName)) return "PUT";
if (annotationName.endsWith("DeleteMapping") || annotationName.endsWith("DELETE") || "DELETE".equals(annotationName)) return "DELETE";
if (annotationName.endsWith("PatchMapping") || annotationName.endsWith("PATCH") || "PATCH".equals(annotationName)) return "PATCH";
if (annotationName.endsWith("RequestMapping")) return "REQUEST";
return null;
}

View File

@@ -0,0 +1,46 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class GenericEventDetectorFireTest {
@Test
void shouldDetectFireMethodAsTrigger(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
enum OrderEvent { PAY, SHIP }
class OrderService {
private final StateMachineFacade facade;
OrderService(StateMachineFacade facade) { this.facade = facade; }
public void pay() {
facade.fire(OrderEvent.PAY);
}
}
class StateMachineFacade {
public void fire(OrderEvent event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
GenericEventDetector detector = new GenericEventDetector(context, context.getConstantResolver(), List.of());
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.OrderService");
List<TriggerPoint> triggers = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
assertThat(triggers).hasSize(1);
assertThat(triggers.get(0).getMethodName()).isEqualTo("pay");
assertThat(triggers.get(0).getEvent()).contains("PAY");
}
}

View File

@@ -0,0 +1,78 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class MessagingDetectorTest {
@Test
void shouldDetectRabbitListenerQueue(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class ReturnsListener {
@RabbitListener(queues = "returns.queue")
public void onReturn(String orderId) {}
}
""";
Files.writeString(tempDir.resolve("ReturnsListener.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
MessagingDetector detector = new MessagingDetector(context);
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.ReturnsListener");
List<EntryPoint> entryPoints = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
assertThat(entryPoints).hasSize(1);
assertThat(entryPoints.get(0).getType()).isEqualTo(EntryPoint.Type.RABBIT);
assertThat(entryPoints.get(0).getName()).isEqualTo("RABBIT: returns.queue");
assertThat(entryPoints.get(0).getMetadata()).containsEntry("destination", "returns.queue");
}
@Test
void shouldDetectKafkaListenerTopicsAndTopicPattern(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class OrderKafkaListener {
@KafkaListener(topics = {"orders.created", "orders.updated"})
public void onOrderEvent(String payload) {}
@KafkaListener(topicPattern = "payments.*")
public void onPaymentEvent(String payload) {}
}
""";
Files.writeString(tempDir.resolve("OrderKafkaListener.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
MessagingDetector detector = new MessagingDetector(context);
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.OrderKafkaListener");
List<EntryPoint> entryPoints = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
assertThat(entryPoints).hasSize(2);
assertThat(entryPoints).anyMatch(ep ->
ep.getType() == EntryPoint.Type.KAFKA
&& ep.getName().contains("orders.created")
&& ep.getName().contains("orders.updated"));
assertThat(entryPoints).anyMatch(ep ->
ep.getType() == EntryPoint.Type.KAFKA
&& "payments.*".equals(ep.getMetadata().get("destination")));
}
}

View File

@@ -0,0 +1,74 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class SpringMvcDetectorTest {
@Test
void shouldDetectWebFluxRouterFunctionBean(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
public class ReactiveOrderApi {
@Bean
public RouterFunction<ServerResponse> orderRoutes() {
return route().POST("/reactive/order", request -> ServerResponse.ok().build()).build();
}
}
""";
Files.writeString(tempDir.resolve("ReactiveOrderApi.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
SpringMvcDetector detector = new SpringMvcDetector(context, context.getConstantResolver());
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.ReactiveOrderApi");
List<EntryPoint> entryPoints = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
assertThat(entryPoints).anyMatch(ep ->
"POST /reactive/order".equals(ep.getName())
&& "orderRoutes".equals(ep.getMethodName())
&& "WebFlux Functional".equals(ep.getMetadata().get("style")));
}
@Test
void shouldDetectRestControllerPostMapping(@TempDir Path tempDir) throws Exception {
String source = """
package com.example;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping("/pay")
public void pay() {}
}
""";
Files.writeString(tempDir.resolve("OrderController.java"), source);
CodebaseContext context = new CodebaseContext();
context.setResolveBindings(true);
context.scan(tempDir);
SpringMvcDetector detector = new SpringMvcDetector(context, context.getConstantResolver());
org.eclipse.jdt.core.dom.TypeDeclaration td = context.getTypeDeclaration("com.example.OrderController");
List<EntryPoint> entryPoints = detector.detect((org.eclipse.jdt.core.dom.CompilationUnit) td.getRoot());
assertThat(entryPoints).anyMatch(ep ->
"POST /api/orders/pay".equals(ep.getName()) && "pay".equals(ep.getMethodName()));
}
}