diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/AnonymousClassBugTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/AnonymousClassBugTest.java new file mode 100644 index 0000000..110a2e1 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/AnonymousClassBugTest.java @@ -0,0 +1,78 @@ +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 java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AnonymousClassBugTest { + + @Test + void shouldResolveEventThroughAnonymousClass() throws IOException { + String source = """ + package com.example; + + public class MyController { + private StateMachine machine; + + public void process() { + // Supplying the event via an anonymous class + fireEvent(new EventProvider() { + @Override + public TransitionEnum getEvent() { + return TransitionEnum.STATE_A; + } + }); + } + + private void fireEvent(EventProvider provider) { + machine.fire(provider.getEvent()); + } + } + + interface EventProvider { + TransitionEnum getEvent(); + } + + class StateMachine { + public void fire(TransitionEnum event) {} + } + + enum TransitionEnum { STATE_A, STATE_B } + """; + + Path tempDir = Files.createTempDirectory("callgraph_test_anonymous"); + Files.writeString(tempDir.resolve("Config.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.MyController") + .methodName("process") + .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("Poly events from anonymous class: " + chains.get(0).getTriggerPoint().getPolymorphicEvents()); + + // It SHOULD resolve to STATE_A by tracking the anonymous class implementation + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_A"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/InheritedMethodBugTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/InheritedMethodBugTest.java new file mode 100644 index 0000000..1d0f0f8 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/InheritedMethodBugTest.java @@ -0,0 +1,71 @@ +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 java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InheritedMethodBugTest { + + @Test + void shouldResolveEventFromInheritedProtectedMethodWithoutThis() throws IOException { + String source = """ + package com.example; + + public class ChildController extends ParentController { + private StateMachine machine; + + public void process() { + // Calling inherited protected method WITHOUT 'this.' or 'super.' + machine.fire(getInheritedEvent()); + } + } + + abstract class ParentController { + protected TransitionEnum getInheritedEvent() { + return TransitionEnum.STATE_P; // Protected method returns STATE_P + } + } + + class StateMachine { + public void fire(TransitionEnum event) {} + } + + enum TransitionEnum { STATE_P, STATE_Q } + """; + + Path tempDir = Files.createTempDirectory("callgraph_test_inherited"); + Files.writeString(tempDir.resolve("Config.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.ChildController") + .methodName("process") + .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("Poly events from inherited method: " + chains.get(0).getTriggerPoint().getPolymorphicEvents()); + + // It SHOULD resolve to STATE_P by finding getInheritedEvent() in ParentController + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_P"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/LambdaBugTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/LambdaBugTest.java new file mode 100644 index 0000000..4388058 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/LambdaBugTest.java @@ -0,0 +1,71 @@ +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 java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LambdaBugTest { + + @Test + void shouldResolveEventThroughLambda() throws IOException { + String source = """ + package com.example; + + import java.util.function.Supplier; + + public class MyController { + private StateMachine machine; + + public void process() { + // Supplying the event via a lambda + fireEvent(() -> TransitionEnum.STATE_L); + } + + private void fireEvent(Supplier eventSupplier) { + machine.fire(eventSupplier.get()); + } + } + + class StateMachine { + public void fire(TransitionEnum event) {} + } + + enum TransitionEnum { STATE_L, STATE_M } + """; + + Path tempDir = Files.createTempDirectory("callgraph_test_lambda"); + Files.writeString(tempDir.resolve("Config.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.MyController") + .methodName("process") + .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("Poly events from lambda: " + chains.get(0).getTriggerPoint().getPolymorphicEvents()); + + // It SHOULD resolve to STATE_L by tracking the lambda supplier + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_L"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/OverloadedConstructorBugTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/OverloadedConstructorBugTest.java new file mode 100644 index 0000000..1860ced --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/OverloadedConstructorBugTest.java @@ -0,0 +1,86 @@ +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 java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OverloadedConstructorBugTest { + + @Test + void shouldResolveEventThroughOverloadedConstructors() throws IOException { + String source = """ + package com.example; + + public class MyController { + private StateMachine machine; + + public void process() { + // Creating command via 1-arg constructor + MyCommand command = new MyCommand(TransitionEnum.STATE_Z); + machine.fire(command.getEvent()); + } + } + + class MyCommand { + private final TransitionEnum event; + private final String metadata; + + // 1-arg constructor calls 2-arg constructor using this(...) + public MyCommand(TransitionEnum event) { + this(event, "default_metadata"); + } + + // 2-arg constructor assigns the field + public MyCommand(TransitionEnum event, String metadata) { + this.event = event; + this.metadata = metadata; + } + + public TransitionEnum getEvent() { + return event; + } + } + + class StateMachine { + public void fire(TransitionEnum event) {} + } + + enum TransitionEnum { STATE_X, STATE_Y, STATE_Z } + """; + + Path tempDir = Files.createTempDirectory("callgraph_test_constructor"); + Files.writeString(tempDir.resolve("Config.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.MyController") + .methodName("process") + .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("Poly events from overloaded constructor: " + chains.get(0).getTriggerPoint().getPolymorphicEvents()); + + // It SHOULD resolve to STATE_Z by following the 'this(...)' call in the constructor + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_Z"); + } +} diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/StaticFactoryBugTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/StaticFactoryBugTest.java new file mode 100644 index 0000000..255bc13 --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/StaticFactoryBugTest.java @@ -0,0 +1,84 @@ +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 java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class StaticFactoryBugTest { + + @Test + void shouldResolveEventThroughStaticFactory() throws IOException { + String source = """ + package com.example; + + public class MyController { + private StateMachine machine; + + public void process() { + // Creating command via static factory method + MyCommand command = MyCommand.of(TransitionEnum.STATE_W); + machine.fire(command.getEvent()); + } + } + + class MyCommand { + private final TransitionEnum event; + + // Private constructor + private MyCommand(TransitionEnum event) { + this.event = event; + } + + // Static factory method + public static MyCommand of(TransitionEnum event) { + return new MyCommand(event); + } + + public TransitionEnum getEvent() { + return event; + } + } + + class StateMachine { + public void fire(TransitionEnum event) {} + } + + enum TransitionEnum { STATE_W, STATE_X, STATE_Y, STATE_Z } + """; + + Path tempDir = Files.createTempDirectory("callgraph_test_static_factory"); + Files.writeString(tempDir.resolve("Config.java"), source); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.MyController") + .methodName("process") + .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("Poly events from static factory: " + chains.get(0).getTriggerPoint().getPolymorphicEvents()); + + // It SHOULD resolve to STATE_W by following the static factory and its internal instantiation + assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("TransitionEnum.STATE_W"); + } +}