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;
|
if (classNeighbor.equals(classTarget)) return true;
|
||||||
|
|
||||||
String simpleClassNeighbor = classNeighbor.contains(".") ? classNeighbor.substring(classNeighbor.lastIndexOf('.') + 1) : classNeighbor;
|
if (context.areSameTypeOrUnambiguousSimpleMatch(classNeighbor, classTarget)) return true;
|
||||||
String simpleClassTarget = classTarget.contains(".") ? classTarget.substring(classTarget.lastIndexOf('.') + 1) : classTarget;
|
|
||||||
|
|
||||||
if (simpleClassNeighbor.equals(simpleClassTarget)) return true;
|
|
||||||
|
|
||||||
if (context.areClassesPolymorphicallyCompatible(classNeighbor, classTarget)) {
|
if (context.areClassesPolymorphicallyCompatible(classNeighbor, classTarget)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -245,6 +245,17 @@ public class TypeResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (actualType.equals(expectedType)) return true;
|
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;
|
if (actualType.endsWith("." + expectedType) || expectedType.endsWith("." + actualType)) return true;
|
||||||
|
|
||||||
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(actualType);
|
AbstractTypeDeclaration td = context.getAbstractTypeDeclaration(actualType);
|
||||||
|
|||||||
@@ -538,6 +538,25 @@ public class CodebaseContext {
|
|||||||
return ambiguousSimpleNames.contains(name);
|
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) {
|
public List<String> getEnumValues(String fqnOrSimpleName) {
|
||||||
if (fqnOrSimpleName == null) return null;
|
if (fqnOrSimpleName == null) return null;
|
||||||
|
|
||||||
@@ -826,9 +845,7 @@ public class CodebaseContext {
|
|||||||
if (classNeighbor.equals(classTarget)) {
|
if (classNeighbor.equals(classTarget)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
String simpleClassNeighbor = simpleTypeName(classNeighbor);
|
if (areSameTypeOrUnambiguousSimpleMatch(classNeighbor, classTarget)) {
|
||||||
String simpleClassTarget = simpleTypeName(classTarget);
|
|
||||||
if (simpleClassNeighbor.equals(simpleClassTarget)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return classCompatibilityCache.areClassesCompatible(
|
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", "e")).isEqualTo(-1);
|
||||||
assertThat(resolver.getParameterIndex("com.example.StateMachine.sendEvent", "payload")).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");
|
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
|
@Test
|
||||||
void shouldFindClassWithFullyQualifiedAnnotation() throws IOException {
|
void shouldFindClassWithFullyQualifiedAnnotation() throws IOException {
|
||||||
String source = """
|
String source = """
|
||||||
|
|||||||
Reference in New Issue
Block a user