fix(kotlin): a call on an imported type reaches that package's class when two share a name

When several classes share a name, resolveMethodOnType picks the one the
call file imports by matching the import's path against the candidate's
file path (com/example/b/FooConverter.kt). That holds for Java, where a
public class names its file, but a Kotlin class often lives in a file
named otherwise (FooConverter in Converters.kt), so no candidate matched
and the call went to whichever class came first.

The imported name is now matched against each candidate's qualified
name first: import com.example.b.FooConverter expects the method
com.example.b::FooConverter::convert, which the package directive gives
every JVM declaration whatever its file is called. The file-path match
stays as the fallback.

The Java calls this once fixed, a Java file calling a Kotlin class it
imports (okhttp's Java tests and mockwebserver3.MockWebServer,
SystemUI's BlurUtils), are now resolved on main by the receiver's
declared type (#1942), so on today's main the change shows in Kotlin.

From #539 we take only this change and its test. Its other two parts,
indexing Kotlin class properties and reading Kotlin imports without a
trailing semicolon, have since landed on main another way.

Validation:
- resolution.test.ts: the Kotlin same-name case (#314's shape in
  Converters.kt files) is red on main, green here.
- A/B against main 40f58517 (with the Java receiver typing of #1942
  and the Kotlin changes of #1933, #1761 and #2258): no edge changes
  on okhttp, SystemUI, nowinandroid, mihon, jextract, spring-modulith,
  Minestom, spring-ai, kestra-java, playframework, click or gin; none
  of them has the test's shape on a resolved call. Against main
  4eb8c2a5, before #1942, it moved the 8 Java calls above onto the
  Kotlin class their file imports; main now reaches them at 0.9.
- Cost (mihon, median of 3 interleaved indexes, on main 4eb8c2a5): CPU
  25.7 s on main vs 25.2 s, wall 8.2 s vs 8.3 s.

Original PR #539 by @arttttt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
arttttt
2026-10-29 16:56:48 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent 40f5851787
commit 86c85cff02
3 changed files with 72 additions and 0 deletions
+17
View File
@@ -13622,6 +13622,23 @@ export function resolveMethodOnType(
if (chosen) return { original: ref, targetNodeId: chosen.id, confidence, resolvedBy };
}
if (matches.length > 1 && preferredFqn && ref.language !== 'go' && !ES_IMPORT_LANGUAGES.has(ref.language)) {
// Prefer the candidate whose namespace-qualified name matches the imported
// FQN. #412 indexes JVM declarations as `<pkg>::<Class>` from the file's
// `package` directive — independent of the file name — so this disambiguates
// Kotlin (where `FooConverter` can live in `Converters.kt`) that the
// file-path-suffix heuristic below can't. `com.example.b.FooConverter`
// → expected method qn `com.example.b::FooConverter::convert`.
const lastDot = preferredFqn.lastIndexOf('.');
if (lastDot > 0) {
const expectedQn = `${preferredFqn.slice(0, lastDot)}::${preferredFqn.slice(lastDot + 1)}::${methodName}`;
const byQn = matches.find((m) => m.qualifiedName === expectedQn);
if (byQn) {
return { original: ref, targetNodeId: byQn.id, confidence, resolvedBy };
}
}
// Fall back to file-path-suffix match for Java's class-name = file-name
// layout (covers files indexed before the namespace wrapper, or any case
// the qn match misses).
const ext = ref.language === 'kotlin' ? '.kt' : '.java';
const fqnPath = preferredFqn.replace(/\./g, '/') + ext;
const chosen = matches.find((m) => {