Add regression for ambiguous enum simple-name linking
Ensures the call graph and linker fail closed when multiple enums share the same simple name across packages and bindings are unavailable. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user