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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user