diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/AmbiguousSimpleEnumLinkingTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/AmbiguousSimpleEnumLinkingTest.java new file mode 100644 index 0000000..c8a5eee --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/AmbiguousSimpleEnumLinkingTest.java @@ -0,0 +1,97 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher; +import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult; +import click.kamil.springstatemachineexporter.analysis.model.CallChain; +import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata; +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +import click.kamil.springstatemachineexporter.analysis.model.LinkResolution; +import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import click.kamil.springstatemachineexporter.model.Event; +import click.kamil.springstatemachineexporter.model.State; +import click.kamil.springstatemachineexporter.model.Transition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Regression: when two enums share the same simple name across packages, we must not expand or link + * by guessing which enum {@code OrderEvent} refers to when bindings are unavailable. + */ +class AmbiguousSimpleEnumLinkingTest { + + @Test + void shouldFailClosedWhenEnumSimpleNameIsAmbiguousAcrossPackages(@TempDir Path tempDir) throws Exception { + Files.createDirectories(tempDir.resolve("a")); + Files.createDirectories(tempDir.resolve("b")); + Files.writeString(tempDir.resolve("a/OrderEvent.java"), + "package a; public enum OrderEvent { PAY, SHIP }"); + Files.writeString(tempDir.resolve("b/OrderEvent.java"), + "package b; public enum OrderEvent { CANCEL }"); + Files.writeString(tempDir.resolve("App.java"), """ + package com.example; + import a.OrderEvent; + public class OrderController { + Dispatcher dispatcher = new Dispatcher(); + public void transition(String eventStr) { + dispatcher.dispatch(eventStr); + } + } + class Dispatcher { + void dispatch(String eventStr) { + StateMachine sm = new StateMachine(); + // Unresolved valueOf(arg) must not expand to a concrete enum set via simple-name lookup. + sm.sendEvent(OrderEvent.valueOf(eventStr)); + } + } + class StateMachine { void sendEvent(OrderEvent e) {} } + """); + + CodebaseContext context = new CodebaseContext(); + // Keep bindings off so type printing stays import-style and we rely on scan-time indexes. + context.setResolveBindings(false); + context.scan(tempDir); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + EntryPoint entry = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("transition") + .build(); + TriggerPoint trigger = TriggerPoint.builder() + .className("com.example.StateMachine") + .methodName("sendEvent") + .event("e") + .build(); + + CallChain chain = engine.findChains(List.of(entry), List.of(trigger)).get(0); + + Transition pay = new Transition(); + pay.setSourceStates(List.of(State.of("NEW", "NEW"))); + pay.setTargetStates(List.of(State.of("PAID", "PAID"))); + pay.setEvent(Event.of("PAY", "a.OrderEvent.PAY")); + + Transition ship = new Transition(); + ship.setSourceStates(List.of(State.of("PAID", "PAID"))); + ship.setTargetStates(List.of(State.of("SHIPPED", "SHIPPED"))); + ship.setEvent(Event.of("SHIP", "a.OrderEvent.SHIP")); + + AnalysisResult result = AnalysisResult.builder() + .name("com.example.OrderStateMachineConfig") + .transitions(List.of(pay, ship)) + .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build()) + .build(); + + new TransitionLinkerEnricher().enrich(result, context, null); + + CallChain linked = result.getMetadata().getCallChains().get(0); + assertThat(linked.getMatchedTransitions()).isNullOrEmpty(); + assertThat(linked.getLinkResolution()).isIn(LinkResolution.NO_MATCH, LinkResolution.AMBIGUOUS_WIDEN); + } +} +