diff --git a/CHANGELOG.md b/CHANGELOG.md index 4007a059d..82a703784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -257,6 +257,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Kotlin, a `fun interface` is now indexed as an interface with its methods, like okhttp's `HttpLoggingInterceptor.Logger`. Before, it was read as a broken function, and when a comment followed it, the declaration after it went missing too, its members turning into loose functions. Re-index Kotlin projects after upgrading. Thanks @danusha2345. - In Kotlin, a `private val` or `private var` property, and the members of a `private` class or object, no longer take calls from other files. Properties were indexed without their visibility, so a call like `token(...)` in one file could link to another class's `private var token`, and JavaScript, Go or Python callers could land on a Kotlin test's private property; a call to `now()` could also land on a method of a test's `private class Clock`. Re-index Kotlin projects after upgrading. Thanks @danusha2345. - In Kotlin, a call on a library type you import, like Jetpack Compose's `Modifier.pad()`, now links to the extension your project declares on that type, `fun Modifier.pad()`, when the calling file can see it: in the same package, or imported by name or with `.*`. The library type's own methods, like `Modifier.fillMaxSize()`, still never link to a project method that shares the name. Re-index Kotlin projects after upgrading. Thanks @danusha2345. +- In Kotlin, when two packages each declare a class with the same name, a call on a value of that type now links to the class the file imports, even when that class lives in a file named for something else, like `FooConverter` in `Converters.kt`. Before, it went to whichever of the two came first. Java calls on such a class, like okhttp's Java tests calling the `mockwebserver3.MockWebServer` they import, now link through the type their variable is declared with (#1942). Re-index Kotlin projects after upgrading. Thanks @arttttt. - In JavaScript and TypeScript, a `super.run()` call now links to the method of the class's parent, or of the nearest ancestor that has it, like three.js's `super.copy(source)` reaching `Object3D.copy`, so `codegraph_explore`'s call path and impact follow an override into the method it builds on. Before, such a call inside a method of the same name was dropped, and elsewhere it went to whatever method had the name, often the class's own or a sibling class's in the same file. A call from a static method reaches the parent's static method, and a call to a parent from outside your project, like `HTMLElement`, `Set` or `Promise`, links nothing. Re-index JavaScript and TypeScript projects after upgrading. Thanks @Puma7 for the report and the pull request. (#2497) - In JavaScript and TypeScript, a named class expression, like `register(class Widget extends Base {…})`, `customElements.define('x-card', class XCard extends HTMLElement {…})` or `const Panel = class PanelImpl extends Base {…}`, is now indexed as a class with its own methods and the class it extends, so calls between its methods and its `super` calls link as a class declaration's do. Before, its methods were free functions or weren't indexed at all, and it extended nothing. Its name stays bound inside the expression alone, as JavaScript has it: `new Widget()` elsewhere links nothing, a constant, class or interface of the same name around it keeps its links, and code outside never reaches its methods by a guess. A class a CommonJS module exports, like webpack's `module.exports = class MultiCompiler {…}` or `exports.Store = class Store {…}`, is reached through `require` as a class declaration is, along with its static methods and the methods called on its instances. Re-index JavaScript and TypeScript projects after upgrading. Thanks @Puma7 for the report and the pull request. (#2499) - In Go, a call through a struct field that holds a function, like prometheus's `d.refreshf(ctx)`, now reaches the functions put in that field, so `codegraph_explore` follows service discovery from `Discovery.Run` into each provider's `refresh` and names the line where each provider wires its method in, like `RefreshF: d.refresh`. The field can be set in a struct literal or by an assignment, copied from another struct's field (`refreshf: opts.RefreshF`), or given a method of an interface value, and two unrelated structs with a field of the same name, like `fn` or `handler`, stay apart. Before, such a flow stopped at the field. Re-index Go projects after upgrading. diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index 197ebd481..e20d2d997 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -1930,6 +1930,60 @@ public class Handler { ); }); + it('Kotlin import disambiguates same-name classes in non-eponymous files (#314)', async () => { + // The Kotlin twist on #314: the class lives in a file NOT named after + // it (`Converters.kt`, the idiomatic Kotlin layout), so the file-path + // suffix heuristic (`com/example/b/FooConverter.kt`) can't match — only + // the namespace `qualifiedName` (`com.example.b::FooConverter`, from the + // package directive) disambiguates. + const aDir = path.join(tempDir, 'module-a/src/main/kotlin/com/example/a'); + const bDir = path.join(tempDir, 'module-b/src/main/kotlin/com/example/b'); + const webDir = path.join(tempDir, 'web/src/main/kotlin/com/example/web'); + fs.mkdirSync(aDir, { recursive: true }); + fs.mkdirSync(bDir, { recursive: true }); + fs.mkdirSync(webDir, { recursive: true }); + + fs.writeFileSync( + path.join(aDir, 'Converters.kt'), + `package com.example.a +class FooConverter { fun convert(x: String): String { return "a:" + x } } +` + ); + fs.writeFileSync( + path.join(bDir, 'Converters.kt'), + `package com.example.b +class FooConverter { fun convert(x: String): String { return "b:" + x } } +` + ); + // Caller imports the module-b converter; module-a is lexically first in + // the candidate list, so a correct result proves the import wins. + fs.writeFileSync( + path.join(webDir, 'Handler.kt'), + `package com.example.web + +import com.example.b.FooConverter + +class Handler { + private val fooConverter: FooConverter = FooConverter() + fun use(): String { return fooConverter.convert("input") } +} +` + ); + + cg = await CodeGraph.init(tempDir, { index: true }); + + const use = cg + .getNodesByKind('method') + .find((n) => n.qualifiedName.endsWith('Handler::use')); + expect(use).toBeDefined(); + const calls = cg.getOutgoingEdges(use!.id).filter((e) => e.kind === 'calls'); + expect(calls.length).toBeGreaterThanOrEqual(1); + + const target = cg.getNode(calls[0]!.target); + expect(target?.name).toBe('convert'); + expect(target?.qualifiedName).toBe('com.example.b::FooConverter::convert'); + }); + it('C# extracts references from method/property/field types (#381)', async () => { // Pre-#381, every C# project produced ZERO `references` edges: // csharp.ts was missing returnField, and the type-leaf walker diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index d08d38637..03ab98b31 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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 `::` 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) => {