Skip to content

fix(kotlin): resolve imported type calls by qualified name - #1948

Open
mixxer wants to merge 4 commits into
colbymchenry:mainfrom
mixxer:fix/kotlin-imported-type-calls
Open

mixxer wants to merge 4 commits into
colbymchenry:mainfrom
mixxer:fix/kotlin-imported-type-calls

Conversation

@mixxer

@mixxer mixxer commented Sep 24, 2026 •

Copy link
Copy Markdown

Summary

Resolve Kotlin calls through imported types and their nested classes, including imported outer types, while rejecting unimported receiver types. Preserve receiver-scoped extension lookup for external imported types such as Modifier.pad instead of exiting before extension matching.

Validation

  • Rebased on upstream 6560052a6f856855d3f71eee838fd66ccfa4285d; tested head a8425277350d91ee8fcc594cdd677724046ae74f.
  • macOS / Node 24.21.0: build passed; full suite: 442 passed / 16 skipped files; 5,713 passed, 263 skipped (portable backend); final test-fixture type correction also rechecked.
  • TypeScript language server: no new diagnostics relative to upstream; changed source files have no errors or warnings. Existing upstream test diagnostics are tracked separately.
  • Linux CI: all 11 jobs passed, including this exact PR head on Node 24. Native kernel checks also passed for fix: preserve Java and Kotlin anonymous inheritance #1927 and fix(java): resolve calls through static fields #1949; integrated main passed on Node 22 and 24.

@danusha2345

Copy link
Copy Markdown
Contributor

I ran this against main (ba3c21e) on three Kotlin Android apps and one Kotlin/Java/C app.

Edge diff. 9 edges were removed and 2 added, and all 11 changes are correct:

  • LayoutInflater.from(...) no longer lands on a project TransportType::from (4 edges).
  • Log.d/i/w/e inside a Logx wrapper no longer loops back to Logx (5).
  • NativeBridge.ensureLoaded/sendfd now resolve to the Java class (2).

The three Kotlin-only apps are unchanged. The build and full suite pass. On main only the nested-constructor assertion fails; the android, androidx, alias, direct, extension and value cases already pass there.

Regressions: four edges that main resolves correctly are dropped. Once the type is explicitly imported, only methods declared on that type or extensions in the same file are accepted, and the return null then blocks every later fallback. Minimal repros:

  1. Companion extension in another file. With fun Factory.Companion.extMake() in p/Ext.kt and Factory.extMake() in q/Calls.kt, main resolves to Factory::extMake; the PR gives no edge.
  2. Inherited member of an imported object. With object Obj : Base() and import p.Obj, main resolves Obj.hello() to p::Base::hello; the PR gives no edge.
  3. Project extension on an imported library type. With fun Modifier.pad() in the project and import androidx.compose.ui.Modifier, main resolves Modifier.pad() to Modifier::pad; the PR gives no edge. This is very common in Compose code.
  4. Extension on an imported object, declared in another file. fun Modes.ext() in p/Ext.kt, called as Modes.ext(), gives no edge.

Suggested fix. When the imported type is in the index, fall through to the existing logic instead of returning null. Return null only when the type is external and no project extension is declared on that receiver name.

Minor points:

  • kotlinImportedType re-scans the file text with a regex, twice per call, although getImportMappings/importedFqnOf already hold the imports.
  • An import line with a trailing comment doesn't match, which falls back to main's behaviour.
  • A CHANGELOG entry under [Unreleased] is missing.

It merges cleanly with #1933, and the two are complementary. #1933 types receivers from property and local declarations; it doesn't cover LayoutInflater.from on an imported class, which this PR fixes.

@mixxer mixxer reopened this Sep 25, 2026
@mixxer

mixxer commented Sep 25, 2026

Copy link
Copy Markdown
Author

Addressed the four reported regressions in d30590c. Imported project types can continue through inherited-member resolution; visible project extensions remain eligible, including companion, object, cross-file, and external-type extensions. An extension not imported into the caller stays unresolved. The import check now accepts a trailing line comment, and [Unreleased] has a changelog entry.

The test fixture imports cross-package extensions explicitly because Kotlin's package/import rules make imports file-local; overload resolution considers accessible members and extensions in scope, with members taking priority. This is also consistent with the reference-target and scope model exposed by Kotlin's Analysis API. That is a design comparison, not a claim about Kotlin LSP's closed implementation.

I kept the local import scan for now: getImportMappings currently routes Kotlin through the Java parser, which requires a semicolon and does not parse Kotlin aliases, so it cannot replace this check without a broader import-parser change. The focused Kotlin/resolution/framework tests passed (242), the full suite passed (4,536 passed, 192 skipped), and npm run build passed. The branch merges cleanly with #1933 and current main.

@mixxer

mixxer commented Sep 25, 2026

Copy link
Copy Markdown
Author

Fork follow-up: mixxer/codegraph-aosp#8 carries the reviewer regression fixes on top of the fork's merged Kotlin PR. Its Node 22/24, native kernel/AOSP, and final CI checks all passed.

@danusha2345

Copy link
Copy Markdown
Contributor

Re-checked d30590c against main (ba3c21e), with the native kernel built.

All four reported regressions are fixed: Factory.extMake() (companion extension in another file), Obj.hello() (inherited through an imported object), Modifier.pad() (project extension on an imported library type), and Modes.ext(). The controls are unchanged (named companion, valueOf, typealias, Java inherited static, nested import). A few cases are now better than main:

  • An external import whose name matches a project method (TaskStackBuilder.create(), Log.d()) no longer binds to an unrelated project method.
  • When two packages declare the same extension, the imported one wins: with import r.dup, the call now goes to r's dup, not p's.

Corpora: three Kotlin-only Android apps are unchanged. A Kotlin/Java/C app has 9 lost and 2 gained, all 11 correct, the same as the previous head.

Tests: the Kotlin-related suites plus resolution.test.ts pass with the kernel (1,226). The CHANGELOG entry is there.

It also merges cleanly with #1933 and the rest of our open resolver work, with no behavioural conflicts. LGTM.

The only conflict is textual, with #1949, in CHANGELOG.md and two additive hunks in name-matcher.ts (the helpers after importedFqnOf and matchReference), so whichever lands second needs a rebase.

danusha2345 added a commit to danusha2345/codegraph that referenced this pull request Sep 25, 2026
…by qualified name

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mixxer

mixxer commented Sep 26, 2026

Copy link
Copy Markdown
Author

Thanks for rechecking the native-kernel cases and the corpus diff. I agree with the result; I don't see another code change needed from this review. I'll resolve the textual conflict with #1949 on whichever PR lands second.

@mixxer

mixxer commented Sep 26, 2026

Copy link
Copy Markdown
Author

Fork integration status: mixxer/codegraph-aosp#8 is merged at 2fd8267. Its post-merge main CI run passed Node 22/24, native kernel/AOSP, and CI checks. I have no additional code change from the LGTM review; upstream #1948 remains available for review at d30590c.

@danusha2345

Copy link
Copy Markdown
Contributor

Re-checked against main (34ede4d). It still merges cleanly, and main now covers 9 of the 12 assertions in kotlin-imported-type-call.test.ts (the aliased import via #2196). Still missing on main: Outer.Inner() through an imported Java outer class gets no edge, and Modes.hidden() still links to an extension the file does not import; merged, this PR fixes both. But its own test then fails on Modifier.pad(): #2196's outside-import rule (import androidx.compose.ui.Modifier owns the name) returns before matchMethodCall, so the project's own fun Modifier.pad() gets no edge either way — that one is a regression on main for Compose-style extensions, not something this PR introduced. Moving the visible-extension check into or ahead of that rule would keep it.

🤖 Generated with Claude Code

danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 3, 2026
…n on it

colbymchenry#2196 made a Kotlin import from outside the project own its name, so
`Modifier.fillMaxSize()` after `import androidx.compose.ui.Modifier` no
longer lands on a same-named project method. The rule returns before any
other strategy, so a project extension on that type - Compose's
`fun Modifier.pad()`, indexed as `Modifier::pad` - could not be reached
by `Modifier.pad()` either (nor `Color.fromHex()` on a
`fun Color.Companion.fromHex()`).

Inside the rule, a Kotlin `Type.member` call now resolves to a project
extension `Type::member` that the call site can see (same file or
package, or imported by name or `.*`, as for other top-level Kotlin
declarations) and whose own file imports the same type. Anything else
stays unlinked, as before. The visibility check follows the idea of
`kotlinExtensionInScope` from the discussion in colbymchenry#1948.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mixxer
mixxer force-pushed the fix/kotlin-imported-type-calls branch from d30590c to a842527 Compare October 4, 2026 15:30
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 6, 2026
…n on it

colbymchenry#2196 made a Kotlin import from outside the project own its name, so
`Modifier.fillMaxSize()` after `import androidx.compose.ui.Modifier` no
longer lands on a same-named project method. The rule returns before any
other strategy, so a project extension on that type - Compose's
`fun Modifier.pad()`, indexed as `Modifier::pad` - could not be reached
by `Modifier.pad()` either (nor `Color.fromHex()` on a
`fun Color.Companion.fromHex()`).

Inside the rule, a Kotlin `Type.member` call now resolves to a project
extension `Type::member` that the call site can see (same file or
package, or imported by name or `.*`, as for other top-level Kotlin
declarations) and whose own file imports the same type. Anything else
stays unlinked, as before. The visibility check follows the idea of
`kotlinExtensionInScope` from the discussion in colbymchenry#1948.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 7, 2026
…n on it

colbymchenry#2196 made a Kotlin import from outside the project own its name, so
`Modifier.fillMaxSize()` after `import androidx.compose.ui.Modifier` no
longer lands on a same-named project method. The rule returns before any
other strategy, so a project extension on that type - Compose's
`fun Modifier.pad()`, indexed as `Modifier::pad` - could not be reached
by `Modifier.pad()` either (nor `Color.fromHex()` on a
`fun Color.Companion.fromHex()`).

Inside the rule, a Kotlin `Type.member` call now resolves to a project
extension `Type::member` that the call site can see (same file or
package, or imported by name or `.*`, as for other top-level Kotlin
declarations) and whose own file imports the same type. Anything else
stays unlinked, as before. The visibility check follows the idea of
`kotlinExtensionInScope` from the discussion in colbymchenry#1948.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 7, 2026
…n on it

colbymchenry#2196 made a Kotlin import from outside the project own its name, so
`Modifier.fillMaxSize()` after `import androidx.compose.ui.Modifier` no
longer lands on a same-named project method. The rule returns before any
other strategy, so a project extension on that type - Compose's
`fun Modifier.pad()`, indexed as `Modifier::pad` - could not be reached
by `Modifier.pad()` either (nor `Color.fromHex()` on a
`fun Color.Companion.fromHex()`).

Inside the rule, a Kotlin `Type.member` call now resolves to a project
extension `Type::member` that the call site can see (same file or
package, or imported by name or `.*`, as for other top-level Kotlin
declarations) and whose own file imports the same type. Anything else
stays unlinked, as before. The visibility check follows the idea of
`kotlinExtensionInScope` from the discussion in colbymchenry#1948.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants