Fail closed on ambiguous simple-name type compatibility

Reject cross-package class matching and enum parameter compatibility when
types share an ambiguous simple name but differ by package-qualified FQN.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 05:46:54 +02:00
parent 82f4e5c087
commit 6401a58c20
5 changed files with 66 additions and 7 deletions

View File

@@ -361,10 +361,7 @@ public class CallGraphPathFinder {
if (classNeighbor.equals(classTarget)) return true;
String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
if (context.areSameTypeOrUnambiguousSimpleMatch(classNeighbor, classTarget)) return true;
if (context.areClassesPolymorphicallyCompatible(classNeighbor, classTarget)) {
return true;

View File

@@ -245,6 +245,17 @@ public class TypeResolver {
}
if (actualType.equals(expectedType)) return true;
String expectedSimple = expectedType.contains(".")
? expectedType.substring(expectedType.lastIndexOf('.') + 1)
: expectedType;
String actualSimple = actualType.contains(".")
? actualType.substring(actualType.lastIndexOf('.') + 1)
: actualType;
if (expectedSimple.equals(actualSimple) && context.isAmbiguousSimpleName(expectedSimple)) {
return false;
}
if (actualType.endsWith("." + expectedType) || expectedType.endsWith("." + actualType)) return true;
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(actualType);

View File

@@ -538,6 +538,25 @@ public class CodebaseContext {
return ambiguousSimpleNames.contains(name);
}
/**
* True when two type references denote the same type, or share an unambiguous simple name.
* Fails closed when the shared simple name exists in multiple packages.
*/
public boolean areSameTypeOrUnambiguousSimpleMatch(String leftFqn, String rightFqn) {
if (leftFqn == null || rightFqn == null) {
return false;
}
if (leftFqn.equals(rightFqn)) {
return true;
}
String leftSimple = simpleTypeName(leftFqn);
String rightSimple = simpleTypeName(rightFqn);
if (!leftSimple.equals(rightSimple)) {
return false;
}
return !isAmbiguousSimpleName(leftSimple);
}
public List<String> getEnumValues(String fqnOrSimpleName) {
if (fqnOrSimpleName == null) return null;
@@ -826,9 +845,7 @@ public class CodebaseContext {
if (classNeighbor.equals(classTarget)) {
return true;
}
String simpleClassNeighbor = simpleTypeName(classNeighbor);
String simpleClassTarget = simpleTypeName(classTarget);
if (simpleClassNeighbor.equals(simpleClassTarget)) {
if (areSameTypeOrUnambiguousSimpleMatch(classNeighbor, classTarget)) {
return true;
}
return classCompatibilityCache.areClassesCompatible(

View File

@@ -30,4 +30,22 @@ class TypeResolverTest {
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "e")).isEqualTo(-1);
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "payload")).isEqualTo(-1);
}
@Test
void shouldFailClosedOnAmbiguousSimpleTypeCompatibility(@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 }");
Files.writeString(tempDir.resolve("b/OrderEvent.java"),
"package b; public enum OrderEvent { SHIP }");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
TypeResolver resolver = new TypeResolver(context);
assertThat(resolver.isTypeCompatible("a.OrderEvent", "OrderEvent")).isFalse();
assertThat(resolver.isTypeCompatible("a.OrderEvent", "b.OrderEvent")).isFalse();
assertThat(resolver.isTypeCompatible("a.OrderEvent", "a.OrderEvent")).isTrue();
}
}

View File

@@ -275,6 +275,22 @@ class CodebaseContextTest {
assertThat(context.getImplementations("b.Service")).containsExactly("b.BServiceImpl");
}
@Test
void areSameTypeOrUnambiguousSimpleMatchShouldFailClosedForAmbiguousTypes() throws IOException {
Files.createDirectories(tempDir.resolve("a"));
Files.createDirectories(tempDir.resolve("b"));
Files.writeString(tempDir.resolve("a/Dispatcher.java"),
"package a; public class Dispatcher { public void dispatch() {} }");
Files.writeString(tempDir.resolve("b/Dispatcher.java"),
"package b; public class Dispatcher { public void dispatch() {} }");
CodebaseContext context = new CodebaseContext();
context.scan(tempDir);
assertThat(context.areSameTypeOrUnambiguousSimpleMatch("a.Dispatcher", "b.Dispatcher")).isFalse();
assertThat(context.areClassesPolymorphicallyCompatible("a.Dispatcher", "b.Dispatcher")).isFalse();
}
@Test
void shouldFindClassWithFullyQualifiedAnnotation() throws IOException {
String source = """