diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java index ba33933..6a716a7 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/CallGraphPathFinder.java @@ -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; diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java index 648fc4d..c6c3522 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolver.java @@ -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); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java index 6869e00..4251643 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContext.java @@ -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 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( diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolverTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolverTest.java index 6940774..b936564 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolverTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/TypeResolverTest.java @@ -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(); + } } diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContextTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContextTest.java index 1e5c336..347ddc9 100644 --- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContextTest.java +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/ast/common/CodebaseContextTest.java @@ -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 = """