better things
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user