From a463291f9a25bac970767bb02fc2a28548df9b53 Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Fri, 17 Jul 2026 08:48:09 +0200 Subject: [PATCH] C --- .../ast/common/CodebaseContext.java | 116 ++++++++++++------ .../ast/common/CodebaseContextTest.java | 26 ++++ 2 files changed, 102 insertions(+), 40 deletions(-) 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 3a3ccb2..ed749da 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 @@ -476,46 +476,8 @@ public class CodebaseContext { return; } - // Try direct match - List directImpls = interfaceToImpls.get(cleanName); - String directImplsSource = directImpls != null ? "direct" : null; - - // Try FQN match if input was simple name - if (directImpls == null) { - String fqn = simpleNameToFqn.get(typeName); - if (fqn != null) { - directImpls = interfaceToImpls.get(fqn); - if (directImpls != null) { - directImplsSource = "simple→fqn"; - } - } - } - - // Try simple name match if input was FQN - if (directImpls == null && cleanName.contains(".")) { - String simpleName = cleanName.substring(cleanName.lastIndexOf('.') + 1); - List simpleImpls = interfaceToImpls.get(simpleName); - if (simpleImpls != null) { - if (ambiguousSimpleNames.contains(simpleName)) { - String pkg = cleanName.substring(0, cleanName.lastIndexOf('.')); - List pkgScoped = new ArrayList<>(); - for (String impl : simpleImpls) { - if (impl.startsWith(pkg + ".")) { - pkgScoped.add(impl); - } - } - directImpls = pkgScoped.isEmpty() ? null : pkgScoped; - } else { - directImpls = simpleImpls; - } - if (directImpls != null) { - directImplsSource = "fqn→simple"; - } - } - } - if (directImplsSource == null) { - directImplsSource = "none"; - } + List directImpls = resolveDirectImplementations(typeName, cleanName); + String directImplsSource = describeDirectImplsSource(typeName, cleanName); if (log.isDebugEnabled()) { log.debug( @@ -535,6 +497,80 @@ public class CodebaseContext { } } + /** + * Union direct subclasses/implementors from every index bucket that refers to the same supertype. + * Generated stubs often index under a literal FQN ({@code extends abcd.ccc.AML}) while hand-written + * sources index under the simple name ({@code extends AML}) — merge instead of stopping at the first hit. + */ + private List resolveDirectImplementations(String typeName, String cleanName) { + LinkedHashSet merged = new LinkedHashSet<>(); + if (cleanName == null || cleanName.isBlank()) { + return null; + } + + addImplsForKey(merged, cleanName); + + if (!cleanName.contains(".")) { + String fqn = simpleNameToFqn.get(typeName != null ? typeName : cleanName); + if (fqn != null && !fqn.equals(cleanName)) { + addImplsForKey(merged, fqn); + } + } else { + String simpleName = cleanName.substring(cleanName.lastIndexOf('.') + 1); + if (!ambiguousSimpleNames.contains(simpleName)) { + addImplsForKey(merged, simpleName); + } else { + String pkg = cleanName.substring(0, cleanName.lastIndexOf('.')); + List simpleImpls = interfaceToImpls.get(simpleName); + if (simpleImpls != null) { + for (String impl : simpleImpls) { + if (impl.startsWith(pkg + ".")) { + merged.add(impl); + } + } + } + } + } + + return merged.isEmpty() ? null : new ArrayList<>(merged); + } + + private void addImplsForKey(Set merged, String key) { + List impls = interfaceToImpls.get(key); + if (impls != null) { + merged.addAll(impls); + } + } + + private String describeDirectImplsSource(String typeName, String cleanName) { + if (cleanName == null || cleanName.isBlank()) { + return "none"; + } + boolean hasDirect = interfaceToImpls.containsKey(cleanName); + boolean hasFqnAlias = false; + boolean hasSimpleAlias = false; + if (!cleanName.contains(".")) { + String fqn = simpleNameToFqn.get(typeName != null ? typeName : cleanName); + hasFqnAlias = fqn != null && interfaceToImpls.containsKey(fqn); + } else { + String simpleName = cleanName.substring(cleanName.lastIndexOf('.') + 1); + hasSimpleAlias = interfaceToImpls.containsKey(simpleName); + } + if (hasDirect && (hasFqnAlias || hasSimpleAlias)) { + return "merged"; + } + if (hasDirect) { + return "direct"; + } + if (hasFqnAlias) { + return "simple→fqn"; + } + if (hasSimpleAlias) { + return "fqn→simple"; + } + return "none"; + } + private void indexEnum(EnumDeclaration ed, String parentFqn, Path javaFile) { String simpleName = ed.getName().getIdentifier(); String fqn = parentFqn.isEmpty() ? simpleName : parentFqn + "." + simpleName; 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 ad54df3..ca67fb2 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,32 @@ class CodebaseContextTest { assertThat(context.getImplementations("b.Service")).containsExactly("b.BServiceImpl"); } + @Test + void getImplementationsShouldMergeFqnAndSimpleNameIndexBuckets() throws IOException { + Files.createDirectories(tempDir.resolve("abcd/ccc")); + Files.writeString(tempDir.resolve("abcd/ccc/AML.java"), """ + package abcd.ccc; + public abstract class AML {} + """); + Files.writeString(tempDir.resolve("abcd/ccc/RealListener.java"), """ + package abcd.ccc; + public class RealListener extends AML {} + """); + // KAPT-style stub: no import, FQN in extends clause + Files.writeString(tempDir.resolve("abcd/ccc/Av.java"), """ + package abcd.ccc; + public class Av extends abcd.ccc.AML {} + """); + + CodebaseContext context = new CodebaseContext(); + context.scan(tempDir); + + assertThat(context.getImplementations("abcd.ccc.AML")) + .containsExactlyInAnyOrder("abcd.ccc.Av", "abcd.ccc.RealListener"); + assertThat(context.getImplementations("AML")) + .containsExactlyInAnyOrder("abcd.ccc.Av", "abcd.ccc.RealListener"); + } + @Test void areSameTypeOrUnambiguousSimpleMatchShouldFailClosedForAmbiguousTypes() throws IOException { Files.createDirectories(tempDir.resolve("a"));