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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
Reference in New Issue
Block a user