tests reorg

This commit is contained in:
2026-06-27 14:42:33 +02:00
parent 101f2b5dc9
commit e8543ace38
6 changed files with 3115 additions and 3102 deletions

View File

@@ -0,0 +1,957 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Tag;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEngineGetterTest {
@Test
void shouldResolveConstantsFromAlternativeGetterName() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.get2Type());
}
}
class DomainEvent {
public String get2Type() {
return "ALTERNATIVE_EVENT";
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_alt_getter");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ALTERNATIVE_EVENT");
}
@Test
void shouldResolveConstantsFromInlineInstantiation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent().getEvent());
}
}
class DomainEvent {
public String getEvent() { return "INLINE_EVENT"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithArguments() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent("PAYLOAD_ID").getEvent());
}
}
class DomainEvent {
private String id;
public DomainEvent(String id) { this.id = id; }
public String getEvent() { return "INLINE_EVENT_WITH_ARGS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_args");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_ARGS");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithGenerics() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new DomainEvent<String>().getEvent());
}
}
class DomainEvent<T> {
public String getEvent() { return "INLINE_EVENT_WITH_GENERICS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_gen");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_GENERICS");
}
@Test
void shouldResolveConstantsFromInlineInstantiationInheritedMethod() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new SpecificDomainEvent().getEvent());
}
}
abstract class BaseDomainEvent {
public String getEvent() { return "BASE_EVENT_RETURN"; }
}
class SpecificDomainEvent extends BaseDomainEvent {
// no getEvent here, it inherits it!
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_hier");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
}
@Test
void shouldResolveConstantsFromInlineInstantiationWithComplexArguments() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(Order o, String abc) {
service.process(new DomainEvent(o.getId(), abc).getEvent());
}
}
class Order {
public String getId() { return "123"; }
}
class DomainEvent {
private String id;
private String extra;
public DomainEvent(String id, String extra) { this.id = id; this.extra = extra; }
public String getEvent() { return "INLINE_EVENT_COMPLEX_ARGS"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_complex_args");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_COMPLEX_ARGS");
}
@Test
void shouldNotIncludeSubclassesForInlineInstantiation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new BaseEvent().getEvent());
}
}
class BaseEvent {
public String getEvent() { return "BASE_EVENT_RETURN"; }
}
class SubEvent extends BaseEvent {
public String getEvent() { return "SUB_EVENT_RETURN"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_inline_inst_subclass");
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);
// Only BASE_EVENT_RETURN should be found. SUB_EVENT_RETURN is a subclass and should not be included for direct instantiations
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
}
@Test
void shouldResolveConstantsFromFieldAssignedInConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new OrderPickedUpEvent().getType());
}
}
class OrderPickedUpEvent {
private String type;
public OrderPickedUpEvent() {
this.type = "PICKED_UP_IN_CONSTRUCTOR";
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_field_const");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("PICKED_UP_IN_CONSTRUCTOR");
}
@Test
void shouldResolveConstantsFromFieldAssignedInSuperConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new OrderCancelledEvent().getType());
}
}
abstract class BaseEvent {
private String type;
public BaseEvent(String type, String otherVar) {
this.type = type;
}
public String getType() { return type; }
}
class OrderCancelledEvent extends BaseEvent {
public OrderCancelledEvent() {
super("CANCELLED_IN_SUPER", "someVar");
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_super_const");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_IN_SUPER");
}
@Test
void shouldResolveConstantsFromFieldAssignedToAnotherConstantInConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new EventWithIndirectConstant().getType());
}
}
class EventConstants {
public static final String INDIRECT_EVENT = "INDIRECT_EVENT_VALUE";
}
class EventWithIndirectConstant {
private String type;
public EventWithIndirectConstant() {
this.type = EventConstants.INDIRECT_EVENT;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_indirect_const");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("INDIRECT_EVENT_VALUE");
}
@Test
void shouldResolveConstantsFromConstructorInvocation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new EventWithThisCall().getType());
}
}
class EventWithThisCall {
private String type;
public EventWithThisCall() {
this("THIS_CALL_CONSTANT");
}
public EventWithThisCall(String type) {
this.type = type;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_this_const");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("THIS_CALL_CONSTANT");
}
@Test
void shouldResolveConstantsFromDynamicMethodAssignmentInConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new OrderCancelledEvent().getType());
}
}
class OrderCancelledEvent {
private String cancelEventType;
public OrderCancelledEvent() {
this.cancelEventType = assertMatchingCancelEventType("some_sender");
}
private String assertMatchingCancelEventType(String sender) {
return "CANCELLED_BY_SENDER";
}
public String getType() { return cancelEventType; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_dynamic_rhs");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_BY_SENDER");
}
@Test
void shouldResolveConstantsFromFieldDeclarationsAndInitializers() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus1() {
service.process(new EventWithFieldDecl().getType());
}
public void handleStatus2() {
service.process(new EventWithInitBlock().getType());
}
}
class EventWithFieldDecl {
private String event = "FIELD_DECL";
public String getType() { return event; }
}
class EventWithInitBlock {
private String event;
{
event = "INIT_BLOCK";
}
public String getType() { return event; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_field_init");
Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint ep1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus1").build();
EntryPoint ep2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus2").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
List<CallChain> chains = builder.findChains(List.of(ep1, ep2), List.of(trigger));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
java.util.List<String> allEvents = new java.util.ArrayList<>();
chains.forEach(c -> allEvents.addAll(c.getTriggerPoint().getPolymorphicEvents()));
org.assertj.core.api.Assertions.assertThat(allEvents).containsExactlyInAnyOrder("FIELD_DECL", "INIT_BLOCK");
}
@Test
void shouldFallbackToScraperForComplexInlineInstantiations() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
// The AST tracer will trace EventWithConstructorArg.getType()
// It will find `this.type = typeArg`, which it cannot resolve because it's a parameter.
// The fallback scraper should catch "COMPLEX_INLINE_CONST".
service.process(new EventWithConstructorArg("COMPLEX_INLINE_CONST", 123).getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_scraper");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST");
}
@Test
void shouldResolveEventFromConstructorAssignment() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
service.process(e.getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_constructor_assignment");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_CONSTRUCTOR");
}
@Test
void shouldPrioritizeSetterOverConstructor() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
e.setType("EVENT_FROM_SETTER");
service.process(e.getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public void setType(String t) { this.type = t; }
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_setter_priority");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_SETTER");
}
@Test
void shouldResolveEventFromFieldConstructorAssignment() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
private EventWithConstructorArg e;
public OrderController() {
this.e = new EventWithConstructorArg("EVENT_FROM_FIELD", 123);
}
public void handleStatus() {
service.process(this.e.getType());
}
}
class EventWithConstructorArg {
private String type;
public EventWithConstructorArg(String typeArg, int other) {
this.type = typeArg;
}
public String getType() { return type; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_field_assignment");
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));
System.out.println("DEBUG CHAINS: " + (chains.isEmpty() ? "empty" : chains.get(0).getTriggerPoint().getPolymorphicEvents()));
org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_FIELD");
}
@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 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!
}
@Test
void shouldResolveEventsWhenFieldTransformerMethodMatchesSetterPatternButArgIsNonConstant() throws IOException {
String source = """
package com.example;
public class OrderController {
private Transformer transformer;
public void processEvent(String dto) {
Event e = transformer.transform(dto);
machine.fire(e.getType());
}
}
interface Transformer {
Event transform(String input);
}
class TransformerImpl implements Transformer {
public Event transform(String input) {
return new Event("ORDER_DISPATCHED");
}
}
class Event {
private String type;
public Event(String type) { this.type = type; }
public String getType() { return type; }
}
class Machine {
public void fire(String event) {}
}
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_transformer");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processEvent")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.Machine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
// Must resolve through TransformerImpl.transform() → "ORDER_DISPATCHED",
// NOT return empty polymorphicEvents due to early exit on the setter match.
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("ORDER_DISPATCHED");
}
@Test
void shouldResolveEnumPassedThroughWrapperObjectAndGetter() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderMapper mapper;
private StateMachine machine;
public void processOrder(String dto) {
OrderEvent event = mapper.toEvent(dto);
machine.fire(event.getType());
}
}
interface OrderMapper {
OrderEvent toEvent(String input);
}
class OrderMapperImpl implements OrderMapper {
public OrderEvent toEvent(String input) {
return new OrderEvent(States.PAYMENT_RECEIVED);
}
}
class OrderEvent {
private States type;
public OrderEvent(States type) { this.type = type; }
public States getType() { return type; } // bare field, no this.
}
class StateMachine {
public void fire(States event) {}
}
enum States { PAYMENT_RECEIVED, CANCELLED }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_wrapper");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrder")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("DIAGNOSTIC polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("States.PAYMENT_RECEIVED");
}
@Test
void shouldResolveTransformingGetterThatMapsDtoEnumToStateMachineEnum() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderMapper mapper;
private StateMachine machine;
public void processOrder(String dto) {
OrderMsg msg = mapper.toMsg(dto);
machine.fire(msg.getSmEvent());
}
}
interface OrderMapper {
OrderMsg toMsg(String input);
}
class OrderMapperImpl implements OrderMapper {
public OrderMsg toMsg(String input) {
return new OrderMsg(OrderType.PAY);
}
}
class OrderMsg {
private OrderType type;
public OrderMsg(OrderType type) { this.type = type; }
public SMEvent getSmEvent() {
return switch (type) {
case PAY -> SMEvent.PAY_RECEIVED;
case CANCEL -> SMEvent.ORDER_CANCELLED;
};
}
}
class StateMachine {
public void fire(SMEvent event) {}
}
enum OrderType { PAY, CANCEL }
enum SMEvent { PAY_RECEIVED, ORDER_CANCELLED }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_twoenum");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderController")
.methodName("processOrder")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("TWO-ENUM polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
// Must return the SM enum, NOT the DTO enum (OrderType.PAY would be wrong)
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("SMEvent.PAY_RECEIVED");
}
}

View File

@@ -0,0 +1,409 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Tag;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEnginePolymorphicTest {
@Test
void shouldResolvePolymorphicInheritanceAcrossChain() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
doProcessOrderEvent(domainEvent);
}
private void doProcessOrderEvent(RichOrderEvent domainEvent) {
OrderEvents eventType = assertSupportedOrderEvent(domainEvent.getType());
service.updateOrderState(eventType);
}
private OrderEvents assertSupportedOrderEvent(OrderEvents e) {
return e;
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {
// sendEvent
}
}
interface RichOrderEvent {
OrderEvents getType();
}
class CancelOrderEvent implements RichOrderEvent {
public OrderEvents getType() { return OrderEvents.CANCELLED; }
}
class ReceiveOrderEvent implements RichOrderEvent {
public OrderEvents getType() { return OrderEvents.RECEIVED; }
}
enum OrderEvents { CREATE, PAY, CANCELLED, RECEIVED }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_poly");
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())
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
.containsExactlyInAnyOrder("OrderEvents.CANCELLED", "OrderEvents.RECEIVED");
}
@Test
void shouldResolveTernaryAndStringPolymorphicReturns() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent() {
service.updateOrderState(new MysteryPayload().getType());
}
}
class OrderService {
public void updateOrderState(Object event) {
// sendEvent
}
}
class MysteryPayload {
public Object getType() {
int a = 5;
if (a > 10) {
return "RAW_STRING_EVENT";
}
return a > 5 ? OrderEvents.ABCD : OrderEvents.PAY;
}
}
enum OrderEvents { ABCD, PAY }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_ternary_string");
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())
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
.containsExactlyInAnyOrder("OrderEvents.ABCD", "OrderEvents.PAY", "RAW_STRING_EVENT");
}
@Test
void shouldResolvePolymorphicInheritanceThroughDelegation() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(BaseEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
interface BaseEvent {
OrderEvents getType();
}
abstract class AbstractEvent implements BaseEvent {
protected OrderEvents internalGetType() {
return OrderEvents.PAY;
}
}
class ConcreteEvent extends AbstractEvent {
public OrderEvents getType() {
return internalGetType();
}
}
class AnotherConcreteEvent implements BaseEvent {
public OrderEvents getType() {
return delegateToHelper();
}
private OrderEvents delegateToHelper() {
return OrderEvents.CANCELLED;
}
}
enum OrderEvents { PAY, CANCELLED }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_poly_delegation");
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("OrderEvents.PAY", "OrderEvents.CANCELLED");
}
@Test
void shouldResolveMethodInheritedFromAbstractBaseClass() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(BaseEvent domainEvent) {
service.updateOrderState(domainEvent.getType());
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
interface BaseEvent {
OrderEvents getType();
}
abstract class AbstractEvent implements BaseEvent {
public OrderEvents getType() {
return OrderEvents.PAY;
}
}
// Concrete subclass inherits getType() but doesn't override it!
class InheritingEvent extends AbstractEvent {
}
enum OrderEvents { PAY }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_inherited");
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("OrderEvents.PAY");
}
@Test
void shouldResolveConstantsFromAbstractSuperclass() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus() {
service.process(new PostAcceptanceOrderModificationsRejectedEvent().getType());
}
}
abstract class AbstractPostAcceptanceOrderModificationsEvent {
private String event;
public AbstractPostAcceptanceOrderModificationsEvent(String event) {
this.event = event;
}
public String getType() { return event; }
}
class PostAcceptanceOrderModificationsRejectedEvent extends AbstractPostAcceptanceOrderModificationsEvent {
public PostAcceptanceOrderModificationsRejectedEvent() {
super("POST_ACCEPTANCE_REJECTED");
}
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_post_acc");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("POST_ACCEPTANCE_REJECTED");
}
@Test
void shouldResolveMappedTransitionEnumThroughAbstractBaseClassAndMultipleArgs() throws IOException {
String source = """
package com.example;
public class FlowController {
private CommandFactory factory;
private StateMachine machine;
public void handleRequest(String payload) {
// Creates subclass with multiple args, including intermediate enum
BaseCommand cmd = factory.buildCommand(payload);
// Fires using the mapped enum from the inherited base method
machine.fire(cmd.getTransitionEvent());
}
}
class CommandFactory {
public BaseCommand buildCommand(String payload) {
// Passing multiple args: String, Enum1, String, int
return new WebCommand(payload, SourceType.WEB_API, "meta_v1", 42);
}
}
abstract class BaseCommand {
protected String id;
protected SourceType sourceType; // The intermediate enum
protected String metadata;
public BaseCommand(String id, SourceType sourceType, String metadata) {
this.id = id;
this.sourceType = sourceType;
this.metadata = metadata;
}
// The tricky part: Inherited method doing the mapping
public TransitionEnum getTransitionEvent() {
return switch (this.sourceType) {
case WEB_API -> TransitionEnum.ON_WEB_START;
case MOBILE_APP -> TransitionEnum.ON_MOBILE_START;
case CRON_JOB -> TransitionEnum.ON_SYSTEM_START;
};
}
}
class WebCommand extends BaseCommand {
private int priorityLevel;
public WebCommand(String id, SourceType sourceType, String metadata, int priorityLevel) {
super(id, sourceType, metadata); // Engine must track through super()
this.priorityLevel = priorityLevel;
}
}
class StateMachine {
public void fire(TransitionEnum event) {}
}
enum SourceType { WEB_API, MOBILE_APP, CRON_JOB }
enum TransitionEnum { ON_WEB_START, ON_MOBILE_START, ON_SYSTEM_START }
""";
java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_complex_inheritance");
java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.FlowController")
.methodName("handleRequest")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("fire")
.event("event")
.build();
List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
System.out.println("COMPLEX INHERITANCE polymorphicEvents: " +
chains.get(0).getTriggerPoint().getPolymorphicEvents());
// The assertion to prove the bug is fixed.
// It MUST NOT contain "SourceType.WEB_API"
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("TransitionEnum.ON_WEB_START");
}
}

View File

@@ -0,0 +1,472 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Tag;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEngineTypeTest {
@Test
void shouldResolveLocalSetterAndSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
public void updateOrderState(String vv) {
StateMachine sm = new StateMachine();
MysteryPayload a = new MysteryPayload();
a.setType(my_func(vv));
sm.sendEvent(a.getType());
}
private OrderEvents my_func(String q) {
return switch (q) {
case "a" -> OrderEvents.A8;
case "b" -> OrderEvents.A133;
default -> throw new IllegalArgumentException("Unknown");
};
}
}
enum OrderEvents { A8, A133 }
class MysteryPayload {
private OrderEvents type;
public void setType(OrderEvents type) { this.type = type; }
public OrderEvents getType() { return type; }
}
class StateMachine {
public void sendEvent(OrderEvents event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.OrderService")
.methodName("updateOrderState")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.StateMachine")
.methodName("sendEvent")
.event("event")
.build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
CallChain chain = chains.get(0);
System.out.println("Resolved poly events: " + chain.getTriggerPoint().getPolymorphicEvents());
assertThat(chain.getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133");
}
@Test
void shouldResolveOldStyleSwitchStatementPolymorphism(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
public void updateOrderState(String vv) {
StateMachine sm = new StateMachine();
sm.sendEvent(getEventFromOldSwitch(vv));
}
private OrderEvents getEventFromOldSwitch(String q) {
switch (q) {
case "a": return OrderEvents.A8;
case "b":
case "c": return OrderEvents.A133;
default: return OrderEvents.PAY;
}
}
}
enum OrderEvents { A8, A133, PAY }
class StateMachine {
public void sendEvent(OrderEvents event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
}
@Test
void shouldResolveMultiValueSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
public void updateOrderState(String vv) {
StateMachine sm = new StateMachine();
sm.sendEvent(getEventMultiValue(vv));
}
private OrderEvents getEventMultiValue(String q) {
return switch (q) {
case "a", "b" -> OrderEvents.A8;
case "c" -> OrderEvents.A133;
default -> OrderEvents.PAY;
};
}
}
enum OrderEvents { A8, A133, PAY }
class StateMachine {
public void sendEvent(OrderEvents event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
}
@Test
void shouldResolveSwitchExpressionWithBlockAndYieldPolymorphism(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class OrderService {
public void updateOrderState(String vv) {
StateMachine sm = new StateMachine();
sm.sendEvent(getEventWithYield(vv));
}
private OrderEvents getEventWithYield(String q) {
return switch (q) {
case "a" -> {
System.out.println("Processing a");
yield OrderEvents.A8;
}
case "b" -> { yield OrderEvents.A133; }
default -> OrderEvents.PAY;
};
}
}
enum OrderEvents { A8, A133, PAY }
class StateMachine {
public void sendEvent(OrderEvents event) {}
}
""";
Files.writeString(tempDir.resolve("OrderService.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
.containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
}
@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
void shouldTraceArrayAndCollectionAccessGracefully() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent() {
OrderEvents[] events = new OrderEvents[]{OrderEvents.PAY};
service.updateOrderState(events[0]);
}
}
class OrderService {
public void updateOrderState(OrderEvents event) {}
}
enum OrderEvents { PAY }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_array");
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);
// 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("new OrderEvents[]{OrderEvents.PAY}[0]");
}
@Test
void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.getEventForStatus());
}
}
class DomainEvent {
private OrderStatus status;
public String getEventForStatus() {
switch (status) {
case NEW: return "CREATE_EVENT";
case PAID: return "PAY_EVENT";
default: return "CANCEL_EVENT";
}
}
}
class OrderService {
public void process(String event) {}
}
enum OrderStatus { NEW, PAID }
""";
Path tempDir = Files.createTempDirectory("callgraph_switch");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("CREATE_EVENT", "PAY_EVENT", "CANCEL_EVENT");
}
@Test
void shouldResolveConstantsFromEnumReturn() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(domainEvent.getEventEnum());
}
}
class DomainEvent {
public String getEventEnum() {
return MyEvents.ORDER_PAID.name();
}
}
class OrderService {
public void process(String event) {}
}
enum MyEvents { ORDER_PAID }
""";
Path tempDir = Files.createTempDirectory("callgraph_enum_return");
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);
org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ORDER_PAID");
}
@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 shouldResolveEnumValueOfFromString(@TempDir Path tempDir) throws IOException {
String source = """
package com.example;
public class Endpoint {
private OrderService service;
public void trigger(String eventString) {
service.doSomething(eventString);
}
}
class OrderService {
public void doSomething(String str) {
MyEvents event = mapToEnum(str);
sendEvent(event);
}
private MyEvents mapToEnum(String str) {
return MyEvents.valueOf(str);
}
public void sendEvent(MyEvents e) {}
}
enum MyEvents { A, B }
""";
Files.writeString(tempDir.resolve("Endpoint.java"), source);
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
EntryPoint entryPoint = EntryPoint.builder()
.className("com.example.Endpoint")
.methodName("trigger")
.build();
TriggerPoint trigger = TriggerPoint.builder()
.className("com.example.OrderService")
.methodName("sendEvent")
.event("e")
.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("MyEvents.A", "MyEvents.B");
}
}

View File

@@ -0,0 +1,271 @@
package click.kamil.springstatemachineexporter.analysis.service;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Tag;
import static org.assertj.core.api.Assertions.assertThat;
@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEngineWrapperTest {
@Test
void shouldUnwrapDeepMethodWrappers() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(RichOrderEvent domainEvent) {
OrderEvents eventType = wrap3(wrap2(wrap1(domainEvent.getType())));
service.updateOrderState(eventType);
}
private OrderEvents wrap1(OrderEvents e) { return e; }
private OrderEvents wrap2(OrderEvents e) { return e; }
private OrderEvents wrap3(OrderEvents e) { return e; }
}
class OrderService {
public void updateOrderState(OrderEvents event) { }
}
class RichOrderEvent {
public OrderEvents getType() { return OrderEvents.CREATE; }
}
enum OrderEvents { CREATE }
""";
Path tempDir = Files.createTempDirectory("callgraph_test_deep_wrap");
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())
.as("Resolved event was: " + chain.getTriggerPoint().getEvent())
.containsExactlyInAnyOrder("OrderEvents.CREATE");
}
@Test
void shouldProperlyUnwrapDeeplyNestedWrapperCallsForFix1() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void processOrderEvent(MyEvent event) {
OrderEvents eventType = wrap1(wrap2(wrap3(event.getType())));
service.updateOrderState(eventType);
}
private OrderEvents wrap1(OrderEvents e) { return e; }
private OrderEvents wrap2(OrderEvents e) { return e; }
private OrderEvents wrap3(OrderEvents e) { return e; }
}
class OrderService {
public void updateOrderState(OrderEvents event) { }
}
class MyEvent {
public OrderEvents getType() { return OrderEvents.CREATE; }
}
enum OrderEvents { CREATE }
""";
Path tempDir = Files.createTempDirectory("callgraph_nested_wrap");
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);
assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("OrderEvents.CREATE");
}
@Test
void shouldNotExtractClassInstanceCreationEvenWithComplexArgsForFix2() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void createOrder(String oID) {
service.processOrderEvent(new OrderCancelledEvent(new OrderId(oID)));
}
}
class OrderService {
public void processOrderEvent(OrderCancelledEvent event) { }
}
class OrderCancelledEvent {
public OrderCancelledEvent(OrderId id) {}
}
class OrderId {
public OrderId(String id) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_complex_cic");
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("createOrder").build();
TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("processOrderEvent").event("event").build();
List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
assertThat(chains).hasSize(1);
assertThat(chains.get(0).getTriggerPoint().getEvent()).isEqualTo("new OrderCancelledEvent(new OrderId(oID))");
}
@Test
void shouldNotCrashOnStaticMethodWrapping() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleStatus(DomainEvent domainEvent) {
service.process(String.valueOf(domainEvent.getEvent()));
}
}
class DomainEvent {
public String getEvent() { return "STATIC_WRAP_EVENT"; }
}
class OrderService {
public void process(String event) {}
}
""";
Path tempDir = Files.createTempDirectory("callgraph_static_wrap");
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);
}
@Test
void shouldNotExtractConstantsFromNestedClassInstanceCreations() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleNestedConstructors() {
// The event contains a nested constructor. The heuristic should NOT extract WRONG_EVENT.
OuterEvent e = new OuterEvent("CORRECT_EVENT", new InnerPayload("WRONG_EVENT"));
service.process(e.getEventType());
}
}
class OrderService {
public void process(String event) {}
}
class OuterEvent {
private String eventType;
private InnerPayload payload;
public OuterEvent(String eventType, InnerPayload payload) {
this.eventType = eventType;
this.payload = payload;
}
public String getEventType() { return eventType; }
}
class InnerPayload {
private String someData;
public InnerPayload(String someData) { this.someData = someData; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_nested_constructor");
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("handleNestedConstructors").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");
}
@Test
void shouldNotExtractConstantsFromNestedArraysOfObjects() throws IOException {
String source = """
package com.example;
public class OrderController {
private OrderService service;
public void handleNestedArrays() {
// The event contains a nested array of objects. The heuristic should NOT extract WRONG_EVENT.
OuterEvent e = new OuterEvent("CORRECT_EVENT", new InnerPayload[] { new InnerPayload("WRONG_EVENT") });
service.process(e.getEventType());
}
}
class OrderService {
public void process(String event) {}
}
class OuterEvent {
private String eventType;
private InnerPayload[] payloads;
public OuterEvent(String eventType, InnerPayload[] payloads) {
this.eventType = eventType;
this.payloads = payloads;
}
public String getEventType() { return eventType; }
}
class InnerPayload {
private String someData;
public InnerPayload(String someData) { this.someData = someData; }
}
""";
Path tempDir = Files.createTempDirectory("callgraph_nested_array");
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("handleNestedArrays").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");
}
}