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 calls through imported Kotlin and Java types, including Outer.Inner constructors, while rejecting unimported extensions and preserving companion, object and inherited-member calls. #2258 remains the owner of the outside-import guard and external-type identity/visibility follow-up; its implementation and this PR pass together in a separate integration check. Because #2258 is unmerged, this PR retains its existing library-extension behavior until that change lands.

Validation

@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>

mixxer commented Oct 8, 2026

Copy link
Copy Markdown
Author

@colbymchenry @danusha2345 Thanks for the follow-up in #2258 and for carrying forward the visibility check. Both PRs now change the outside-import guard in matchReferenceInner, so it would help to agree on the split before rebasing.

Could #2258 own that guard and its external-type identity/visibility tests, with #1948 retaining the imported Outer.Inner() constructor case, the unimported Modes.hidden() guard, and the companion/object/inherited-member regression coverage? In particular, #2258's different-Modifier-type negative case should be preserved alongside #1948's imported project-type cases.

Which PR would you prefer to land first, and does that division work for you? This would preserve both sets of regression coverage without maintaining competing fixes for the same outside-import guard.

@danusha2345

Copy link
Copy Markdown
Contributor

@mixxer That division works for me.

I'd land #2258 first: it is the smaller change (one helper plus a one-line change at the guard), so #1948 then only has to drop its own hunk at that guard when rebasing — the rest of #1948 is in matchMethodCall and before step 3, which #2258 doesn't touch.

For what it's worth, I already run the two together in a local build in exactly that shape — #2258's form of the guard, #1948's matchMethodCall block and pre-exact-name guard — and both suites pass side by side (kotlin-extension-on-imported-type.test.ts and kotlin-imported-type-call.test.ts). One thing to check when you rebase: with the guard returning through kotlinExtensionOnImportedType, the extension branch inside #1948's importedType block is only reached for an imported project type, so the library-type half of it may be dead by then.

#2258 currently conflicts with main on the CHANGELOG only; I'll re-sync it once the current batch of merges settles. The order is of course @colbymchenry's call.

mixxer commented Oct 8, 2026

Copy link
Copy Markdown
Author

Thanks @danusha2345 for confirming the split and testing the two suites together. Agreed: #2258 owns the outside-import guard and external-type identity/visibility tests, including the different-Modifier negative case; #1948 keeps the Outer.Inner() constructor case, Modes.hidden() guard, and companion/object/inherited-member coverage.

Landing #2258 first sounds reasonable. I'll wait for @colbymchenry's final order before rebasing. I've also noted the possible unreachable library-type branch in importedType; the imported project-type behavior and both sets of regression tests need to stay intact.

@mixxer
mixxer force-pushed the fix/kotlin-imported-type-calls branch from a842527 to f32a859 Compare October 9, 2026 07:35
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 10, 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 f32a859 to 3a9e746 Compare October 10, 2026 12:53

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