better things

This commit is contained in:
2026-06-21 22:55:39 +02:00
parent 32de0a51c6
commit 07d241a060
7 changed files with 788 additions and 88 deletions

View File

@@ -0,0 +1,61 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
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;
public class AnonymousClassTests {
@TempDir
Path tempDir;
@Test
void shouldResolveEventFromAnonymousClass() throws Exception {
String source = """
package com.example;
public class AnonymousService {
private EventService service;
public void process() {
MyEvent event = new MyEvent() {
@Override
public String getEvent() {
return "ANON_EVENT";
}
};
service.trigger(event.getEvent());
}
}
class EventService {
public void trigger(String ev) {}
}
class MyEvent {
public String getEvent() { return "BASE_EVENT"; }
}
""";
Files.writeString(tempDir.resolve("AnonymousService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("AnonymousService.java")
.sourceModule("com.example")
.event("event.getEvent()")
.lineNumber(12)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.AnonymousService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
assertThat(result.getPolymorphicEvents()).contains("ANON_EVENT");
}
}

View File

@@ -0,0 +1,54 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
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;
public class CollectionIndexingTests {
@TempDir
Path tempDir;
@Test
void shouldResolveEventFromArrayAccess() throws Exception {
String source = """
package com.example;
public class ArrayService {
private EventService service;
public void process() {
String[] events = new String[] {"EVENT_1", "EVENT_2"};
service.trigger(events[0]);
}
}
class EventService {
public void trigger(String ev) {}
}
""";
Files.writeString(tempDir.resolve("ArrayService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("ArrayService.java")
.sourceModule("com.example")
.event("events[0]")
.lineNumber(7)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.ArrayService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
// When using array indexing, we should ideally extract the indexed element or at least all elements in the array
assertThat(result.getPolymorphicEvents()).contains("EVENT_1");
}
}

View File

@@ -1097,7 +1097,7 @@ class HeuristicCallGraphEngineTest {
}
@Test
void shouldTraceLastTextualAssignmentInTryCatchBlocks() throws IOException {
void shouldTraceAllTextualAssignmentsInTryCatchBlocks() throws IOException {
String source = """
package com.example;
public class OrderController {
@@ -1142,9 +1142,69 @@ class HeuristicCallGraphEngineTest {
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
// The ASTVisitor picks up Assignments in textual order.
// CANCELLED is the last one textually in the method.
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CANCELLED");
assertThat(chain.getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("OrderEvents.PAY", "OrderEvents.CANCELLED");
}
@Test
void shouldTraceEventsThroughMapStructInterfaceImpl() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
private OrderMapper mapper = new OrderMapperImpl();
public void processOrderEvent(String eventDtoStr) {
OrderEvent e = mapper.toEvent(eventDtoStr);
service.updateOrderState(e.getEvent());
}
}
class OrderService {
public void updateOrderState(String event) {}
}
interface OrderMapper {
OrderEvent toEvent(String source);
}
class OrderMapperImpl implements OrderMapper {
@Override
public OrderEvent toEvent(String source) {
return new OrderEvent("MAPPED_EVENT");
}
}
class OrderEvent {
private String event;
public OrderEvent(String event) { this.event = event; }
public String getEvent() { return event; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_test_mapstruct");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrderEvent")
.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().getPolymorphicEvents()).containsExactlyInAnyOrder("MAPPED_EVENT");
}
@Test
@@ -1190,7 +1250,7 @@ class HeuristicCallGraphEngineTest {
CallChain chain = chains.get(0);
// We don't dynamically resolve array indexes, but it shouldn't crash.
// It returns the array access expression as a string.
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("events[0]");
assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("new OrderEvents[]{OrderEvents.PAY}[0]");
}
@Test
@@ -2211,6 +2271,44 @@ class HeuristicCallGraphEngineTest {
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_SETTER");
}
@Test
void shouldResolveEventFromArrayAndList() throws IOException {
String source = """
package com.example;
import java.util.List;
public class OrderController {
private OrderService service;
public void handleArray() {
String[] events = {"ARRAY_EVENT"};
service.process(events[0]);
}
public void handleList() {
List<String> listEvents = List.of("LIST_EVENT");
service.process(listEvents.get(0));
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_array");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleArray").build();
EntryPoint entryPoint2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleList").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint1, entryPoint2), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
List<String> resolvedEvents = chains.stream().flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream()).toList();
org.assertj.core.api.Assertions.assertThat(resolvedEvents).containsExactlyInAnyOrder("ARRAY_EVENT", "LIST_EVENT");
}
@Test
void shouldResolveEventFromFieldConstructorAssignment() throws IOException {
String source = """
@@ -2326,4 +2424,190 @@ class HeuristicCallGraphEngineTest {
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ANONYMOUS_EVENT");
}
@Test
void shouldResolveEventFromBranchingAssignments() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(boolean isRefund) {
String event;
if (isRefund) {
event = "REFUND_EVENT";
} else {
event = "CHARGE_EVENT";
}
service.process(event);
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_branch");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
List<String> resolvedEvents = chains.stream().flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream()).toList();
org.assertj.core.api.Assertions.assertThat(resolvedEvents).containsExactlyInAnyOrder("REFUND_EVENT", "CHARGE_EVENT");
}
@Test
void shouldResolveEventFromLombokDataAndAllArgsConstructor() throws IOException {
String source = """
package com.example;
import lombok.Data;
import lombok.AllArgsConstructor;
public class OrderController {
private OrderService service;
public void handleLombok() {
LombokEvent e = new LombokEvent("LOMBOK_EVENT", 1);
service.process(e.getEvent());
}
}
class OrderService {
public void process(String event) {}
}
@Data
@AllArgsConstructor
class LombokEvent {
private String event;
private int id;
}
""";
Path tempDir = Files.createTempDirectory("callgraph_lombok");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleLombok").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("LOMBOK_EVENT");
}
@Test
void shouldResolveEventFromMethodReference() throws IOException {
String source = """
package com.example;
import java.util.List;
public class OrderController {
private OrderService service;
public void handleMethodRef() {
List<RefEvent> events = List.of(new RefEvent("METHOD_REF_EVENT"));
events.forEach(this::delegateProcessing);
}
private void delegateProcessing(RefEvent e) {
service.process(e.getType());
}
}
class OrderService {
public void process(String event) {}
}
class RefEvent {
private String type;
public RefEvent(String type) { this.type = type; }
public String getType() { return type; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_method_ref");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleMethodRef").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("METHOD_REF_EVENT");
}
@Test
void shouldResolveEventFromJavaRecord() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleRecord() {
RecordEvent e = new RecordEvent("RECORD_EVENT", 42);
service.process(e.type());
}
}
class OrderService {
public void process(String event) {}
}
record RecordEvent(String type, int someId) {}
""";
Path tempDir = Files.createTempDirectory("callgraph_record");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleRecord").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("RECORD_EVENT");
}
@Test
void shouldFailHeuristicAndRequireAccurateLombokMapping() throws IOException {
String source = """
package com.example;
import lombok.Data;
import lombok.AllArgsConstructor;
public class OrderController {
private OrderService service;
public void handleLombok() {
MultiEvent e = new MultiEvent("CORRECT_EVENT", "WRONG_EVENT");
service.process(e.getEventType());
}
}
class OrderService {
public void process(String event) {}
}
@Data
@AllArgsConstructor
class MultiEvent {
private String eventType;
private String unrelatedString;
}
""";
Path tempDir = Files.createTempDirectory("callgraph_lombok_heuristic_break");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleLombok").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactly("CORRECT_EVENT"); // If heuristic rips both, it will fail because it contains WRONG_EVENT too!
}
}

View File

@@ -0,0 +1,66 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
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;
public class StreamApiTests {
@TempDir
Path tempDir;
@Test
void shouldHandleStreamApiChains() throws Exception {
String source = """
package com.example;
import java.util.List;
public class StreamService {
private EventService service;
public void process(List<MyEvent> events) {
events.stream()
.filter(e -> e.isValid())
.map(e -> e.toEvent())
.forEach(t -> service.trigger(t.getEvent()));
}
}
class EventService {
public void trigger(String ev) {}
}
class MyEvent {
public boolean isValid() { return true; }
public OrderEvent toEvent() { return new OrderEvent(); }
}
class OrderEvent {
public String getEvent() { return "STREAM_EVENT"; }
}
""";
Files.writeString(tempDir.resolve("StreamService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.EventService")
.methodName("trigger")
.sourceFile("StreamService.java")
.sourceModule("com.example")
.event("t.getEvent()")
.lineNumber(11)
.build();
TriggerPoint result = engine.resolveTriggerPointParameters(trigger, List.of("com.example.StreamService.process", "com.example.EventService.trigger"), java.util.Collections.emptyMap());
System.out.println("TEST RESULT: " + result);
System.out.println("POLY EVENTS: " + result.getPolymorphicEvents());
assertThat(result.getPolymorphicEvents()).isNotNull().containsExactlyInAnyOrder("STREAM_EVENT");
}
}