diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineCoreTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineCoreTest.java new file mode 100644 index 0000000..3b9016d --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineCoreTest.java @@ -0,0 +1,1006 @@ +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 HeuristicCallGraphEngineCoreTest { + + @Test + void shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public class OrderService { + private final EventDispatcher dispatcher = new EventDispatcher(); + + public void processOrder() { + dispatcher.dispatch("order", this::sendEvent); + } + + public void sendEvent(String event) { + // Dummy trigger + } + } + + class EventDispatcher { + public void dispatch(String order, java.util.function.Consumer callback) { + callback.accept("MY_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("processOrder") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.OrderService") + .methodName("sendEvent") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getMethodChain()).containsExactly( + "com.example.OrderService.processOrder", + "com.example.OrderService.sendEvent" + ); + } + + @Test + void shouldExtractContextMachineId(@TempDir Path tempDir) throws IOException { + String source = """ + package com.example; + public class PersisterService { + private final StateMachinePersister persister = new StateMachinePersister(); + + public void updateOrderState(String orderId) { + StateMachine sm = persister.restore(null, "machine:" + orderId); + sm.sendEvent("PAY"); + } + } + + class StateMachinePersister { + public StateMachine restore(Object sm, String contextObj) { + return new StateMachine(); + } + } + + class StateMachine { + public void sendEvent(String event) {} + } + """; + Files.writeString(tempDir.resolve("PersisterService.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.PersisterService") + .methodName("updateOrderState") + .build(); + + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.StateMachine") + .methodName("sendEvent") + .event("PAY") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getContextMachineId()).isNotNull(); + // Since it's tracing constant concatenation: "machine:" + orderId, which might resolve dynamically or string format. + // It should at least be present (it typically extracts 'machine:' + orderId into expression). + } + + @Test + void shouldResolveTriggerPointParametersAcrossChain() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void createOrder() { + service.updateOrderState(OrderEvents.CREATE); + } + } + + class OrderService { + private StateMachine sm; + public void updateOrderState(OrderEvents event) { + sm.sendEvent(event); + } + } + + class StateMachine { + public void sendEvent(Object event) {} + } + + enum OrderEvents { CREATE, PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test"); + 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("updateOrderState") + .event("event") + .build(); + + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CREATE"); + } + + @Test + void shouldResolveTriggerPointLocalVariableAcrossChain() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(MyOrderEvent domainEvent) { + doProcessOrderEvent(domainEvent); + } + + private void doProcessOrderEvent(MyOrderEvent domainEvent) { + OrderEvents eventType = domainEvent.getType(); + service.updateOrderState(eventType); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) { + // sendEvent + } + } + + class MyOrderEvent { + public OrderEvents getType() { return OrderEvents.PAY; } + } + + enum OrderEvents { CREATE, PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test2"); + 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 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.PAY"); + } + + @Test + void shouldHandleMissingImplementationsGracefully() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(UnknownEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + // UnknownEvent is not defined in the source (e.g. from a third-party library) + + enum OrderEvents { CREATE } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_missing_impl"); + 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("domainEvent.getType()"); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()).isNullOrEmpty(); + } + + @Test + void shouldTraceMultipleLocalVariableAssignments() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(RichOrderEvent domainEvent) { + OrderEvents type1 = domainEvent.getType(); + OrderEvents type2 = type1; + OrderEvents type3 = type2; + service.updateOrderState(type3); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class RichOrderEvent { + public OrderEvents getType() { return OrderEvents.RECEIVED; } + } + + enum OrderEvents { RECEIVED } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_multiple_assign"); + 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("OrderEvents.RECEIVED"); + } + + @Test + void shouldStopTracingRecursiveMethodsGracefully() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(RecursiveEvent domainEvent) { + service.updateOrderState(domainEvent.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class RecursiveEvent { + public OrderEvents getType() { + return this.getType(); + } + } + + enum OrderEvents { PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_recursive"); + 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + // Should safely complete without stack overflow and return an empty result + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("domainEvent.getType()"); + assertThat(chain.getTriggerPoint().getPolymorphicEvents()).isEmpty(); + } + + @Test + void shouldHandleChainedMethodCallsGracefully() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(EventWrapper wrapper) { + service.updateOrderState(wrapper.getEvent().getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + class EventWrapper { + public RichEvent getEvent() { return new RichEvent(); } + } + + class RichEvent { + public OrderEvents getType() { return OrderEvents.PAY; } + } + + enum OrderEvents { PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_chained"); + 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + // It might not fully resolve chained method calls, but it must not crash! + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("wrapper.getEvent().getType()"); + } + + @Test + void shouldTraceStaticFieldReferences() throws IOException { + String source = """ + package com.example; + public class OrderController { + private static final OrderEvents CONSTANT_EVENT = OrderEvents.CANCELLED; + private OrderService service; + public void processOrderEvent() { + service.updateOrderState(CONSTANT_EVENT); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + enum OrderEvents { CANCELLED } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_static_field"); + 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CANCELLED"); + } + + @Test + void shouldHandleAnonymousInnerClassesGracefully() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent() { + RichEvent event = new RichEvent() { + public OrderEvents getType() { + return OrderEvents.CREATE; + } + }; + service.updateOrderState(event.getType()); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + interface RichEvent { + OrderEvents getType(); + } + + enum OrderEvents { CREATE } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_anonymous"); + 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + // The engine traces the anonymous class's getType() through the RichEvent interface, + // resolving to OrderEvents.CREATE via the enum return-type fallback. + assertThat(chain.getTriggerPoint().getPolymorphicEvents()) + .containsExactlyInAnyOrder("OrderEvents.CREATE"); + } + + @Test + void shouldLinkCallsInsideLambdasToEnclosingMethod() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent() { + Runnable r = () -> service.updateOrderState(OrderEvents.PAY); + r.run(); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + enum OrderEvents { PAY } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_lambda_scope"); + 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.PAY"); + } + + @Test + void shouldHandleTernaryOperatorsGracefully() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent(boolean flag) { + OrderEvents event = flag ? OrderEvents.PAY : OrderEvents.CANCELLED; + service.updateOrderState(event); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + enum OrderEvents { PAY, CANCELLED } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_ternary"); + 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + assertThat(chains).hasSize(1); + CallChain chain = chains.get(0); + // It should gracefully fallback to the string representation of the ternary operator + assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("flag ? OrderEvents.PAY : OrderEvents.CANCELLED"); + } + + @Test + void shouldTraceAllTextualAssignmentsInTryCatchBlocks() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void processOrderEvent() { + OrderEvents event; + try { + event = OrderEvents.PAY; + } catch (Exception e) { + event = OrderEvents.CANCELLED; + } + service.updateOrderState(event); + } + } + + class OrderService { + public void updateOrderState(OrderEvents event) {} + } + + enum OrderEvents { PAY, CANCELLED } + """; + Path tempDir = Files.createTempDirectory("callgraph_test_try_catch"); + 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 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 shouldResolveMethodReturnConstantFromMultiplePackagesForFix3() throws IOException { + String source1 = """ + package com.example.api; + public interface RichEvent { + String getType(); + } + """; + String source2 = """ + package com.example.impl.a; + import com.example.api.RichEvent; + public class EventA implements RichEvent { + public String getType() { return "EVENT_A"; } + } + """; + String source3 = """ + package com.example.impl.b; + import com.example.api.RichEvent; + public class EventB implements RichEvent { + public String getType() { return "EVENT_B"; } + } + """; + String source4 = """ + package com.example.controller; + import com.example.api.RichEvent; + public class EventController { + private com.example.service.EventService service; + public void handleEvent(RichEvent event) { + service.process(event.getType()); + } + } + """; + String source5 = """ + package com.example.service; + public class EventService { + public void process(String type) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_multi_pkg"); + Files.writeString(tempDir.resolve("RichEvent.java"), source1); + Files.writeString(tempDir.resolve("EventA.java"), source2); + Files.writeString(tempDir.resolve("EventB.java"), source3); + Files.writeString(tempDir.resolve("EventController.java"), source4); + Files.writeString(tempDir.resolve("EventService.java"), source5); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder().className("com.example.controller.EventController").methodName("handleEvent").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.service.EventService").methodName("process").event("type").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + assertThat(chains).hasSize(1); + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B"); + } + + @Test + void shouldGracefullyHandleComplexMethodChainsWithoutCrashing() throws IOException { + String source = """ + package com.example; + import java.util.Optional; + public class OrderController { + private OrderService service; + public void handleStatus(DomainEvent domainEvent) { + service.process(Optional.of(domainEvent).get().getEvent()); + } + } + + class DomainEvent { + public String getEvent() { return "COMPLEX_EVENT"; } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_complex_chain"); + 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + } + + @Test + void shouldResolveConstantsFromStringConcatenation() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus(DomainEvent domainEvent) { + service.process(domainEvent.getCustomEvent()); + } + } + + class DomainEvent { + public static final String PREFIX = "ORDER_"; + public String getCustomEvent() { + return PREFIX + "CREATED"; + } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_concat"); + 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 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_CREATED"); + } + + @Test + void shouldResolveConstantsFromStringToString() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus(DomainEvent domainEvent) { + service.process(domainEvent.getEventString()); + } + } + + class DomainEvent { + public static final String ACTUAL_STRING = "STRING_LITERAL_EVENT"; + public String getEventString() { + return ACTUAL_STRING.toString(); + } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_string_tostring"); + 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 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("STRING_LITERAL_EVENT"); + } + + @Test + void shouldResolveEventFromLambda() throws IOException { + String source = """ + package com.example; + import java.util.List; + public class OrderController { + private OrderService service; + public void handleList(List events) { + events.forEach(e -> service.process(e.getType())); + } + } + + class LambdaEvent { + private String type; + public LambdaEvent(String typeArg) { this.type = typeArg; } + public String getType() { return type; } + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_lambda"); + 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("handleList").build(); + TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); + List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + System.out.println("DEBUG LAMBDA 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()).isNullOrEmpty(); + } + + @Test + void shouldResolveEventFromAnonymousClass() throws IOException { + String source = """ + package com.example; + public class OrderController { + private OrderService service; + public void handleStatus() { + service.process(new EventProvider() { + @Override + public String getEvent() { return "ANONYMOUS_EVENT"; } + }.getEvent()); + } + } + + interface EventProvider { + String getEvent(); + } + + class OrderService { + public void process(String event) {} + } + """; + Path tempDir = Files.createTempDirectory("callgraph_anonymous"); + 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 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("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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); + List resolvedEvents = chains.stream().flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream()).toList(); + org.assertj.core.api.Assertions.assertThat(resolvedEvents).containsExactlyInAnyOrder("REFUND_EVENT", "CHARGE_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 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 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 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"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineGetterTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineGetterTest.java new file mode 100644 index 0000000..82f5735 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineGetterTest.java @@ -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 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 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 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().getEvent()); + } + } + + class DomainEvent { + 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 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 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 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 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 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 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 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 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 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 chains = builder.findChains(List.of(ep1, ep2), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(2); + + java.util.List 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 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 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 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 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 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 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 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 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 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"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEnginePolymorphicTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEnginePolymorphicTest.java new file mode 100644 index 0000000..660e08c --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEnginePolymorphicTest.java @@ -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 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 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 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 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 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 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"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java deleted file mode 100644 index 781e47f..0000000 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTest.java +++ /dev/null @@ -1,3102 +0,0 @@ -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 HeuristicCallGraphEngineTest { - - @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 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 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 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 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 shouldFindCallChainWithLambdaMethodReference(@TempDir Path tempDir) throws IOException { - String source = """ - package com.example; - public class OrderService { - private final EventDispatcher dispatcher = new EventDispatcher(); - - public void processOrder() { - dispatcher.dispatch("order", this::sendEvent); - } - - public void sendEvent(String event) { - // Dummy trigger - } - } - - class EventDispatcher { - public void dispatch(String order, java.util.function.Consumer callback) { - callback.accept("MY_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("processOrder") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.OrderService") - .methodName("sendEvent") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getMethodChain()).containsExactly( - "com.example.OrderService.processOrder", - "com.example.OrderService.sendEvent" - ); - } - - @Test - void shouldExtractContextMachineId(@TempDir Path tempDir) throws IOException { - String source = """ - package com.example; - public class PersisterService { - private final StateMachinePersister persister = new StateMachinePersister(); - - public void updateOrderState(String orderId) { - StateMachine sm = persister.restore(null, "machine:" + orderId); - sm.sendEvent("PAY"); - } - } - - class StateMachinePersister { - public StateMachine restore(Object sm, String contextObj) { - return new StateMachine(); - } - } - - class StateMachine { - public void sendEvent(String event) {} - } - """; - Files.writeString(tempDir.resolve("PersisterService.java"), source); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - - HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); - - EntryPoint entryPoint = EntryPoint.builder() - .className("com.example.PersisterService") - .methodName("updateOrderState") - .build(); - - TriggerPoint trigger = TriggerPoint.builder() - .className("com.example.StateMachine") - .methodName("sendEvent") - .event("PAY") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getContextMachineId()).isNotNull(); - // Since it's tracing constant concatenation: "machine:" + orderId, which might resolve dynamically or string format. - // It should at least be present (it typically extracts 'machine:' + orderId into expression). - } - - @Test - void shouldResolveTriggerPointParametersAcrossChain() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void createOrder() { - service.updateOrderState(OrderEvents.CREATE); - } - } - - class OrderService { - private StateMachine sm; - public void updateOrderState(OrderEvents event) { - sm.sendEvent(event); - } - } - - class StateMachine { - public void sendEvent(Object event) {} - } - - enum OrderEvents { CREATE, PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test"); - 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("updateOrderState") - .event("event") - .build(); - - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CREATE"); - } - - @Test - void shouldResolveTriggerPointLocalVariableAcrossChain() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(MyOrderEvent domainEvent) { - doProcessOrderEvent(domainEvent); - } - - private void doProcessOrderEvent(MyOrderEvent domainEvent) { - OrderEvents eventType = domainEvent.getType(); - service.updateOrderState(eventType); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) { - // sendEvent - } - } - - class MyOrderEvent { - public OrderEvents getType() { return OrderEvents.PAY; } - } - - enum OrderEvents { CREATE, PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test2"); - 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 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.PAY"); - } - - @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 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 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 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 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 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 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 shouldHandleMissingImplementationsGracefully() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(UnknownEvent domainEvent) { - service.updateOrderState(domainEvent.getType()); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - // UnknownEvent is not defined in the source (e.g. from a third-party library) - - enum OrderEvents { CREATE } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_missing_impl"); - 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("domainEvent.getType()"); - assertThat(chain.getTriggerPoint().getPolymorphicEvents()).isNullOrEmpty(); - } - - @Test - void shouldTraceMultipleLocalVariableAssignments() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(RichOrderEvent domainEvent) { - OrderEvents type1 = domainEvent.getType(); - OrderEvents type2 = type1; - OrderEvents type3 = type2; - service.updateOrderState(type3); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - class RichOrderEvent { - public OrderEvents getType() { return OrderEvents.RECEIVED; } - } - - enum OrderEvents { RECEIVED } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_multiple_assign"); - 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getPolymorphicEvents()) - .containsExactlyInAnyOrder("OrderEvents.RECEIVED"); - } - - @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 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 shouldStopTracingRecursiveMethodsGracefully() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(RecursiveEvent domainEvent) { - service.updateOrderState(domainEvent.getType()); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - class RecursiveEvent { - public OrderEvents getType() { - return this.getType(); - } - } - - enum OrderEvents { PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_recursive"); - 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - // Should safely complete without stack overflow and return an empty result - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("domainEvent.getType()"); - assertThat(chain.getTriggerPoint().getPolymorphicEvents()).isEmpty(); - } - - @Test - void shouldHandleChainedMethodCallsGracefully() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(EventWrapper wrapper) { - service.updateOrderState(wrapper.getEvent().getType()); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - class EventWrapper { - public RichEvent getEvent() { return new RichEvent(); } - } - - class RichEvent { - public OrderEvents getType() { return OrderEvents.PAY; } - } - - enum OrderEvents { PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_chained"); - 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - // It might not fully resolve chained method calls, but it must not crash! - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("wrapper.getEvent().getType()"); - } - - @Test - void shouldTraceStaticFieldReferences() throws IOException { - String source = """ - package com.example; - public class OrderController { - private static final OrderEvents CONSTANT_EVENT = OrderEvents.CANCELLED; - private OrderService service; - public void processOrderEvent() { - service.updateOrderState(CONSTANT_EVENT); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - enum OrderEvents { CANCELLED } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_static_field"); - 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.CANCELLED"); - } - - @Test - void shouldHandleAnonymousInnerClassesGracefully() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent() { - RichEvent event = new RichEvent() { - public OrderEvents getType() { - return OrderEvents.CREATE; - } - }; - service.updateOrderState(event.getType()); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - interface RichEvent { - OrderEvents getType(); - } - - enum OrderEvents { CREATE } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_anonymous"); - 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - // The engine traces the anonymous class's getType() through the RichEvent interface, - // resolving to OrderEvents.CREATE via the enum return-type fallback. - assertThat(chain.getTriggerPoint().getPolymorphicEvents()) - .containsExactlyInAnyOrder("OrderEvents.CREATE"); - } - - @Test - void shouldLinkCallsInsideLambdasToEnclosingMethod() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent() { - Runnable r = () -> service.updateOrderState(OrderEvents.PAY); - r.run(); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - enum OrderEvents { PAY } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_lambda_scope"); - 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("OrderEvents.PAY"); - } - - @Test - void shouldHandleTernaryOperatorsGracefully() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent(boolean flag) { - OrderEvents event = flag ? OrderEvents.PAY : OrderEvents.CANCELLED; - service.updateOrderState(event); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - enum OrderEvents { PAY, CANCELLED } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_ternary"); - 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - assertThat(chains).hasSize(1); - CallChain chain = chains.get(0); - // It should gracefully fallback to the string representation of the ternary operator - assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("flag ? OrderEvents.PAY : OrderEvents.CANCELLED"); - } - - @Test - void shouldTraceAllTextualAssignmentsInTryCatchBlocks() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void processOrderEvent() { - OrderEvents event; - try { - event = OrderEvents.PAY; - } catch (Exception e) { - event = OrderEvents.CANCELLED; - } - service.updateOrderState(event); - } - } - - class OrderService { - public void updateOrderState(OrderEvents event) {} - } - - enum OrderEvents { PAY, CANCELLED } - """; - Path tempDir = Files.createTempDirectory("callgraph_test_try_catch"); - 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 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 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 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 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 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 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 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 shouldResolveMethodReturnConstantFromMultiplePackagesForFix3() throws IOException { - String source1 = """ - package com.example.api; - public interface RichEvent { - String getType(); - } - """; - String source2 = """ - package com.example.impl.a; - import com.example.api.RichEvent; - public class EventA implements RichEvent { - public String getType() { return "EVENT_A"; } - } - """; - String source3 = """ - package com.example.impl.b; - import com.example.api.RichEvent; - public class EventB implements RichEvent { - public String getType() { return "EVENT_B"; } - } - """; - String source4 = """ - package com.example.controller; - import com.example.api.RichEvent; - public class EventController { - private com.example.service.EventService service; - public void handleEvent(RichEvent event) { - service.process(event.getType()); - } - } - """; - String source5 = """ - package com.example.service; - public class EventService { - public void process(String type) {} - } - """; - Path tempDir = Files.createTempDirectory("callgraph_multi_pkg"); - Files.writeString(tempDir.resolve("RichEvent.java"), source1); - Files.writeString(tempDir.resolve("EventA.java"), source2); - Files.writeString(tempDir.resolve("EventB.java"), source3); - Files.writeString(tempDir.resolve("EventController.java"), source4); - Files.writeString(tempDir.resolve("EventService.java"), source5); - - CodebaseContext context = new CodebaseContext(); - context.scan(tempDir); - HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context); - - EntryPoint entryPoint = EntryPoint.builder().className("com.example.controller.EventController").methodName("handleEvent").build(); - TriggerPoint trigger = TriggerPoint.builder().className("com.example.service.EventService").methodName("process").event("type").build(); - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - assertThat(chains).hasSize(1); - assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_A", "EVENT_B"); - } - - - @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 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 shouldGracefullyHandleComplexMethodChainsWithoutCrashing() throws IOException { - String source = """ - package com.example; - import java.util.Optional; - public class OrderController { - private OrderService service; - public void handleStatus(DomainEvent domainEvent) { - service.process(Optional.of(domainEvent).get().getEvent()); - } - } - - class DomainEvent { - public String getEvent() { return "COMPLEX_EVENT"; } - } - - class OrderService { - public void process(String event) {} - } - """; - Path tempDir = Files.createTempDirectory("callgraph_complex_chain"); - 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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); - } - - @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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); - } - - - - @Test - void shouldResolveConstantsFromStringConcatenation() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void handleStatus(DomainEvent domainEvent) { - service.process(domainEvent.getCustomEvent()); - } - } - - class DomainEvent { - public static final String PREFIX = "ORDER_"; - public String getCustomEvent() { - return PREFIX + "CREATED"; - } - } - - class OrderService { - public void process(String event) {} - } - """; - Path tempDir = Files.createTempDirectory("callgraph_concat"); - 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 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_CREATED"); - } - - @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 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 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 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 shouldResolveConstantsFromStringToString() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void handleStatus(DomainEvent domainEvent) { - service.process(domainEvent.getEventString()); - } - } - - class DomainEvent { - public static final String ACTUAL_STRING = "STRING_LITERAL_EVENT"; - public String getEventString() { - return ACTUAL_STRING.toString(); - } - } - - class OrderService { - public void process(String event) {} - } - """; - Path tempDir = Files.createTempDirectory("callgraph_string_tostring"); - 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 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("STRING_LITERAL_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 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 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().getEvent()); - } - } - - class DomainEvent { - 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 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 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 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 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 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 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 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 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 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 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 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 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 chains = builder.findChains(List.of(ep1, ep2), List.of(trigger)); - - org.assertj.core.api.Assertions.assertThat(chains).hasSize(2); - - java.util.List 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 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 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 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 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 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 chains = builder.findChains(List.of(entryPoint1, entryPoint2), List.of(trigger)); - - org.assertj.core.api.Assertions.assertThat(chains).hasSize(2); - List 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 = """ - 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 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 shouldResolveEventFromLambda() throws IOException { - String source = """ - package com.example; - import java.util.List; - public class OrderController { - private OrderService service; - public void handleList(List events) { - events.forEach(e -> service.process(e.getType())); - } - } - - class LambdaEvent { - private String type; - public LambdaEvent(String typeArg) { this.type = typeArg; } - public String getType() { return type; } - } - - class OrderService { - public void process(String event) {} - } - """; - Path tempDir = Files.createTempDirectory("callgraph_lambda"); - 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("handleList").build(); - TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build(); - List chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - System.out.println("DEBUG LAMBDA 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()).isNullOrEmpty(); - } - - @Test - void shouldResolveEventFromAnonymousClass() throws IOException { - String source = """ - package com.example; - public class OrderController { - private OrderService service; - public void handleStatus() { - service.process(new EventProvider() { - @Override - public String getEvent() { return "ANONYMOUS_EVENT"; } - }.getEvent()); - } - } - - interface EventProvider { - String getEvent(); - } - - class OrderService { - public void process(String event) {} - } - """; - Path tempDir = Files.createTempDirectory("callgraph_anonymous"); - 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 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("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 chains = builder.findChains(List.of(entryPoint), List.of(trigger)); - - org.assertj.core.api.Assertions.assertThat(chains).hasSize(1); - List 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 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 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 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 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 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 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 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 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 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 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"); - } - - /** - * Regression test for the early-exit bug in {@code resolveTriggerPointParameters}. - * - *

When {@code traceLocalSetter} matches a field method call whose name happens to equal the - * accessor suffix (e.g. {@code mapper.transform(dto)} matching {@code methodName="transform"}), - * and the argument passed to that method is a non-constant runtime parameter, no events are - * extracted. The old condition - *

if (!polymorphicEvents.isEmpty() || !resolvedValue.equals(tp.getEvent()))
- * caused an early return with empty {@code polymorphicEvents} because {@code resolvedValue} had - * already diverged from the original event name. - * - *

After the fix the condition is just {@code !polymorphicEvents.isEmpty()}, so resolution - * continues through {@code getVariableDeclaredType} → {@code resolveMethodReturnConstant}, - * which correctly traces the field's declared type to its implementation and extracts the - * constant returned by {@code TransformerImpl.transform()}. - */ - @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 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"); - } - - /** - * Diagnostic: enum passed to a wrapper object constructor, then retrieved via getter. - * - * Pattern: - * entry: event = mapper.toEvent(dto); machine.fire(event.getType()); - * mapper: OrderMapperImpl.toEvent → new OrderEvent(States.PAYMENT_RECEIVED) - * wrapper: OrderEvent(States type) { this.type = type; } getType() { return type; } - * - * Expected: polymorphicEvents = ["States.PAYMENT_RECEIVED"] - * Tests whether the engine traces: getter → field → constructor arg → constant. - */ - @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 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"); - } - - /** - * Two-enum transformation: a DTO-level enum (OrderType) is passed into a wrapper - * constructor. The wrapper's getter transforms it via a switch expression to a - * state-machine enum (SMEvent). The engine must return the SM enum, NOT the DTO enum. - * - * Pattern: - * entry: msg = mapper.toMsg(dto); machine.fire(msg.getSmEvent()); - * wrapper: OrderMsg(OrderType t) { this.type = t; } - * getSmEvent() { return switch(type) { case PAY -> SMEvent.PAY_RECEIVED; ... }; } - * - * Root cause of bug: engine extracted the constructor arg (OrderType.PAY) instead of - * evaluating the getter's switch body, returning the wrong enum. - */ - @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 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"); - } - /** - * Diagnostic: Complex multi-argument initialization with abstract base class mapping. - * - * Pattern: - * 1. A Factory creates a specific subclass (WebCommand), passing multiple arguments - * including an intermediate enum (SourceType.WEB_API) alongside strings and ints. - * 2. The subclass delegates to an abstract BaseCommand via super(...). - * 3. The base class stores the intermediate enum in a protected field. - * 4. The controller calls an inherited method: cmd.getTransitionEvent() - * 5. The inherited method uses a switch on the protected field to return a TransitionEnum. - * - * Root cause targeted: The engine traces the field back to the constructor and finds - * SourceType.WEB_API. If the engine is buggy, it will eagerly return "SourceType.WEB_API". - * If correct, it will evaluate the switch mapping and return "TransitionEnum.ON_WEB_START". - */ - @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 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"); - } -} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTypeTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTypeTest.java new file mode 100644 index 0000000..1e1f892 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineTypeTest.java @@ -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 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 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 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 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 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 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 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 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 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 chains = builder.findChains(List.of(entryPoint1, entryPoint2), List.of(trigger)); + + org.assertj.core.api.Assertions.assertThat(chains).hasSize(2); + List 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 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"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineWrapperTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineWrapperTest.java new file mode 100644 index 0000000..afd7b57 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/HeuristicCallGraphEngineWrapperTest.java @@ -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 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 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 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 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 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 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"); + } +}