Skip to content

fix(kotlin): resolve member calls through typed receivers and implicit this - #803

Open
kunalgoesbyken wants to merge 1 commit into
trailhq:mainfrom
kunalgoesbyken:fix/kotlin-member-call-receivers
Open

kunalgoesbyken wants to merge 1 commit into
trailhq:mainfrom
kunalgoesbyken:fix/kotlin-member-call-receivers

Conversation

@kunalgoesbyken

Copy link
Copy Markdown
Contributor

Summary

Kotlin calls were resolved by name against functions only, so a call on a constructor property, a parameter, a typed or constructed local, an object or companion, or an unqualified member of the same class produced no edge, while the Java twin of each resolved. #431 reports the constructor-property case; the same missing receiver typing breaks all of them.

Changes

  • bindings.ts: a Kotlin case in defName (so binding scopes line up with extract.ts) and a handleKotlin that binds val/var constructor parameters as fields, function parameters, and typed or constructor-initialised properties and locals. A plain constructor parameter is not visible to the methods and binds nothing. An UpperCamelCase receiver with no binding is the type (Utils.helper(), Registry.create()), as in Swift.
  • A name declared again with a type we cannot read hides the outer binding instead of inheriting it: a lambda parameter, a for variable, a destructured name, an untyped local or property, a function-typed parameter.
  • extract.ts: this.repo.save() reads as a field access, this@Outer.save() as a call on Outer, and a lowercase bare call inside a type resolves to the member first and the top-level function otherwise, as Swift does. A bare call inside apply, run, with or a buildX lambda is not read as a member of the enclosing class, since its receiver is another object.
$ graft callers FooService.handle      # before
handle · method · FooService.kt:L2-L2
  no indexed callers

$ graft callers FooService.handle      # after
handle · method · FooService.kt:L2-L2
  calls ← trigger (Bar.kt:L2-L4)
      3: fooService.handle(42)
  calls ← run (Bar.kt:L7-L9)
      8: service.handle(1)

An extension function, a call on a chained result (items.first().load()) and an overload set that differs only by parameter types stay unresolved.

Testing

On Windows 11 with Node 26:

  • test/graph-kotlin-members.test.ts (new, five tests): property, this. property, parameter, local, object, companion and implicit-this calls; an unknown member, an unbound receiver and a plain constructor parameter resolving to nothing; the shadowing cases; receiver lambdas; and this@Outer. Four of the five fail on main; the second only asserts that nothing resolves.
  • test/graph*, blast*, ask*, generic*, depth*, resolve* and covers* suites: 677 tests, 1 fail. The one failure is discoverWorkspaceChildren follows a symlinked child repo, a symlink permission error on Windows that fails the same way on main.
  • tsc --noEmit passes.

Closes #431

Kotlin calls were resolved by name against functions only, so a call on a
constructor property, a parameter, a typed or constructed local, an
object or companion, or an unqualified member of the same class produced
no edge, while the Java twin of each resolved. `callers FooService.handle`
reported nothing for a service wired by constructor injection (trailhq#431).

Bind `val`/`var` constructor parameters, function parameters, and typed or
constructor-initialised properties and locals, scoped the way the walk
scopes them. Read `this.repo.call()` as a field access, treat an
UpperCamelCase receiver as the type itself (objects, companions), and let
a lowercase bare call inside a type resolve to the member first and the
top-level function otherwise, as Swift does.

A lambda parameter, loop variable, destructured name, untyped local or
function-typed parameter hides a field of the same name, `this@Outer`
means the outer class, and a bare call inside `apply`, `run`, `with` or a
`buildX` lambda is not read as a member of the enclosing class.
@trailhq-graft

trailhq-graft Bot commented Nov 9, 2026

Copy link
Copy Markdown

🌱 graft blast radius

Nothing outside this diff depends on it. 1 area changed; no indexed dependents at depth 2.
Tests: no test reaches Graph Bindings.
Tag: L4XB — Graph Bindings · Nguyen Thanh Dat — Graph Bindings

Who knows this code — 2 people across 1 area
Area Who knows it
Graph Bindings · changed L4XB — 1 commit, last today · Nguyen Thanh Dat — 1 commit, last today

Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no @ has no GitHub handle in its commit email — tag them by hand, or add a .mailmap entry. A suggestion from history, not a CODEOWNERS rule.

Test signal per changed area — 1 ✗

Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.

  • ✗ Graph Bindings — 0 of 14 reached · no test file reaches it
    • not reached: set, shadow, lookup, defName, kotlinDefName, resolveRecvType, isClassNode, visit, …6 more

graft blast · refs/graft/base...HEAD · depth 2 · 3 changed files

Open the interactive graph → — click an area to see the code that changed, and the line that reaches it.

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.

Kotlin: navigation-expression calls through constructor-injected properties aren't linked in callers — same pattern resolves fine in Java

1 participant