Fail closed on ambiguous symbolic enum expansion
Block expandSymbolicPolymorphicEvents from widening <SYMBOLIC: OrderEvent.*> when the simple type name is ambiguous across packages, and emit symbolic markers from the call graph when enum values cannot be resolved. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -125,6 +125,9 @@ public final class MachineEnumCanonicalizer {
|
|||||||
}
|
}
|
||||||
if (pe.startsWith("<SYMBOLIC: ") && pe.endsWith(".*>")) {
|
if (pe.startsWith("<SYMBOLIC: ") && pe.endsWith(".*>")) {
|
||||||
String symbolicType = pe.substring("<SYMBOLIC: ".length(), pe.length() - 3).trim();
|
String symbolicType = pe.substring("<SYMBOLIC: ".length(), pe.length() - 3).trim();
|
||||||
|
if (context != null && context.isAmbiguousSimpleName(symbolicType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!enumTypesMatch(machineEventTypeFqn, symbolicType)) {
|
if (!enumTypesMatch(machineEventTypeFqn, symbolicType)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -990,6 +990,17 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
|
|||||||
if (polymorphicEvents.size() > 1) {
|
if (polymorphicEvents.size() > 1) {
|
||||||
isAmbiguous = true;
|
isAmbiguous = true;
|
||||||
}
|
}
|
||||||
|
if (polymorphicEvents.isEmpty()
|
||||||
|
&& context.isAmbiguousSimpleName(declaredType)
|
||||||
|
&& !tp.isExternal()
|
||||||
|
&& !isRuntimeEnumParameter(exprNode instanceof Expression expression ? expression : null)
|
||||||
|
&& !(keyedMapLookupOnInitializer[0] && pathBoundMapKeyOnInitializer[0])) {
|
||||||
|
String symbolic = "<SYMBOLIC: " + declaredType + ".*>";
|
||||||
|
if (!polymorphicEvents.contains(symbolic)) {
|
||||||
|
polymorphicEvents.add(symbolic);
|
||||||
|
}
|
||||||
|
isAmbiguous = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -513,6 +513,17 @@ public class CodebaseContext {
|
|||||||
return enumValues;
|
return enumValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when multiple types share the same simple name across packages, so simple-name
|
||||||
|
* lookup must fail closed.
|
||||||
|
*/
|
||||||
|
public boolean isAmbiguousSimpleName(String name) {
|
||||||
|
if (name == null || name.isBlank() || name.contains(".")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return ambiguousSimpleNames.contains(name);
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getEnumValues(String fqnOrSimpleName) {
|
public List<String> getEnumValues(String fqnOrSimpleName) {
|
||||||
if (fqnOrSimpleName == null) return null;
|
if (fqnOrSimpleName == null) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,26 @@ class MachineEnumCanonicalizerTest {
|
|||||||
"com.example.order.OrderEvent.PAY");
|
"com.example.order.OrderEvent.PAY");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotExpandSymbolicPlaceholderWhenSimpleEnumNameIsAmbiguous(@TempDir Path tempDir) throws IOException {
|
||||||
|
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 }");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
List<String> expanded = MachineEnumCanonicalizer.expandSymbolicPolymorphicEvents(
|
||||||
|
List.of("<SYMBOLIC: OrderEvent.*>"),
|
||||||
|
"a.OrderEvent",
|
||||||
|
context);
|
||||||
|
|
||||||
|
assertThat(expanded).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldValidateRawNameConsistencyForCanonicalHelpers() {
|
void shouldValidateRawNameConsistencyForCanonicalHelpers() {
|
||||||
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
|
assertThat(MachineEnumCanonicalizer.isRawNameConsistentWithFullIdentifier(
|
||||||
|
|||||||
@@ -157,5 +157,72 @@ class AmbiguousSimpleEnumLinkingTest {
|
|||||||
assertThat(linked.getMatchedTransitions()).isNullOrEmpty();
|
assertThat(linked.getMatchedTransitions()).isNullOrEmpty();
|
||||||
assertThat(linked.getLinkResolution()).isIn(LinkResolution.NO_MATCH, LinkResolution.AMBIGUOUS_WIDEN);
|
assertThat(linked.getLinkResolution()).isIn(LinkResolution.NO_MATCH, LinkResolution.AMBIGUOUS_WIDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailClosedWhenMachineEventTypeWouldExpandAmbiguousSymbolic(@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();
|
||||||
|
sm.sendEvent(OrderEvent.valueOf(eventStr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class StateMachine { void sendEvent(OrderEvent e) {} }
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
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")
|
||||||
|
.eventTypeFqn("a.OrderEvent")
|
||||||
|
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,8 @@ class CodebaseContextTest {
|
|||||||
.isNull();
|
.isNull();
|
||||||
assertThat(context.getEnumValues("a.OrderEvent")).containsExactly("a.OrderEvent.PAY");
|
assertThat(context.getEnumValues("a.OrderEvent")).containsExactly("a.OrderEvent.PAY");
|
||||||
assertThat(context.getEnumValues("b.OrderEvent")).containsExactly("b.OrderEvent.SHIP");
|
assertThat(context.getEnumValues("b.OrderEvent")).containsExactly("b.OrderEvent.SHIP");
|
||||||
|
assertThat(context.isAmbiguousSimpleName("OrderEvent")).isTrue();
|
||||||
|
assertThat(context.isAmbiguousSimpleName("a.OrderEvent")).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user