Skip to content

fix(resolution): resolve a call to the caller's own class member before ranking same-file namesakes - #1952

Closed
danusha2345 wants to merge 3 commits into
colbymchenry:mainfrom
danusha2345:fix/bare-call-enclosing-class
Closed

danusha2345 wants to merge 3 commits into
colbymchenry:mainfrom
danusha2345:fix/bare-call-enclosing-class

Conversation

@danusha2345

Copy link
Copy Markdown
Contributor

Problem

A call to a member of the caller's own class is resolved as if it were ambiguous. This covers a bare call, which means this.m() in Java, Kotlin, C#, Scala, Swift, C++, Dart and Ruby, and an explicit this./self./$this->/super. call. The extractor drops those receivers, so all of them reach the resolver as the bare name.

findBestMatch then breaks ties between same-file candidates by line distance. Nothing asks which class the caller is in, so a sibling class's same-named method wins at 0.4 when it's closer:

  • render() inside RegName.toString() resolves to IPv4.render.
  • super.onCreate() becomes a self-loop.
  • An inherited gcs() inside ModeX::run goes to Copter::gcs instead of Mode::gcs.

I reproduced it on main for Java, Kotlin, C#, Scala, Swift, C++ (inline and out-of-line members), Dart, Ruby, PHP $this->, Python self. and TypeScript this..

Fix

matchEnclosingScopeMember runs in matchByExactName before the ambiguity ceiling and findBestMatch. It reads the receiver back from the call line: bare, a self-receiver, or super. It then walks the caller's scopes from the inside out and returns the first match at 0.9, the same confidence as a typed match:

  1. the enclosing class's own member;
  2. inherited members, only from supertypes that the class's declaration names;
  3. for bare calls only, outer classes and enclosing functions.

Guards:

  • super. skips its own class and never lands on an interface or abstract declaration.
  • A constructor-shaped call isn't bound to a member.
  • Same-named supertypes in another language are ignored.
  • The walk stops before an outer class when the inner class extends a type that isn't indexed, such as Android View or an anonymous class, since that parent might declare the name.

When nothing matches, the existing ranking applies. A class member now wins over a same-named top-level function, which follows the Kotlin, C++ and Swift scoping rules.

Measured (main ba3c21e vs this branch; node counts unchanged everywhere)

corpus changed edges checked
MediatR, Go app, Rust app, small Kotlin app 0 —
Kotlin apps 1 + 1 correct
Flask 1 correct
Kotlin/C/PHP app 4 correct (parent::__construct self-loops ×3, $this->fixlinks)
Slim (PHP) 147 all correct
ArduPilot ArduCopter + 4 libraries (C++) 163 35 checked, all correct
playframework 1,803 retargeted + 112 new ~120 checked, e.g. ResultExtractors::status, FutureAwaits::await, BaseController::Action (previously unresolved or an unrelated spec at 0.4)
decompiled Android SystemUI (Java) 2,813 lost / 2,806 gained ~80 checked: mostly super.x() self-loops and inherited getContext/getChildCount that went to unrelated classes

About 350 changed edges were checked by hand, and none has a wrong target. Indexing time is within noise: SystemUI took 42.7 s and 59.0 s on main and 47.5 s and 61.3 s with this branch, on a loaded machine.

It composes with #1938 and #1936: this path returns before theirs, and all tests pass together.

Known limits:

  • In Kotlin with(x) { render() }, the lambda receiver should win, but the enclosing class is picked. On main this was a proximity guess anyway.
  • Inheritance relies on supertype edges already existing when the ref resolves. If they don't yet, the old ranking applies.

Tests

The new __tests__/enclosing-class-member-call.test.ts has 20 cases:

  • 16 fail on main: the 12 language cases, a Java inner class calling its outer class, an inherited member, super.render(), and same-named nested Builder types.
  • 4 guards: a top-level function with no competing member, an inner class with an unindexed parent, super. never landing on an interface, and Kotlin member vs top-level function.

The full suite passes: 275 files, 4736 tests.

🤖 Generated with Claude Code

@danusha2345
danusha2345 force-pushed the fix/bare-call-enclosing-class branch 2 times, most recently from 6db9949 to 08f14e6 Compare September 30, 2026 18:55
@danusha2345

Copy link
Copy Markdown
Contributor Author

Rebased onto main and narrowed. Main now confines a bare call to what is in scope for Kotlin, C#, Swift, Dart, Ruby and PHP (#2172, #2151, #2170, #2145, #2147, #2142) and Java via #2126, so 13 of the 20 cases pass there and are dropped with their language branches. What remains, all failing on main: Scala and C++ bare calls, Python self. and TS this. (going to B::render instead of the caller's A::render), Java super.render() (no edge on main), and two same-named nested Java classes. One original case — a nested Java class whose supertype is outside the index should not get the outer class's method — is dropped: under #2126 main treats the outer Outer::render as the right answer, and changing that would mean changing main's Java rule.

🤖 Generated with Claude Code

@danusha2345
danusha2345 force-pushed the fix/bare-call-enclosing-class branch 3 times, most recently from c2e1cca to defc548 Compare October 7, 2026 16:08
@danusha2345
danusha2345 force-pushed the fix/bare-call-enclosing-class branch 2 times, most recently from 4bd180a to 158b37a Compare October 15, 2026 17:47
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 16, 2026
…o the argument-fit pick (colbymchenry#1952 follow-up for colbymchenry#2612)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@danusha2345
danusha2345 force-pushed the fix/bare-call-enclosing-class branch from 158b37a to 2dbc55a Compare October 16, 2026 18:53
@danusha2345

Copy link
Copy Markdown
Contributor Author

Rebased onto main (2344ce0) and amended for #2612, whose cpp-free-function-overloads suite this branch failed as it stood: the enclosing-scope member pick ran before the argument-fit choice, so Store(1) in namespace pb took the first Store overload in the file. C++ overloads of one free function in the caller's namespace now fall through to the pick below — the overload the arguments fit, else the nearest — exactly as on main. The rest of the change is unchanged; full suite green on this head.

@danusha2345
danusha2345 force-pushed the fix/bare-call-enclosing-class branch 2 times, most recently from a3c14f0 to 021e954 Compare October 20, 2026 10:13
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 20, 2026
…he name strategies; colbymchenry#1938/colbymchenry#1954 tests follow upstream's colbymchenry#2684/colbymchenry#2664

The same changes went into the rebased PR branches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@danusha2345
danusha2345 force-pushed the fix/bare-call-enclosing-class branch 4 times, most recently from 7ccdd75 to 7776ad1 Compare October 23, 2026 09:21
@danusha2345

Copy link
Copy Markdown
Contributor Author

Rebased on main 16a8e77, with one follow-up for the super-call reading main gained in #2818 (7776ad1):

  • A super.run() inside a class expression's method is recorded from the enclosing method (the expression's members have no nodes), and main's matchJsSuperCall declines to place it. The enclosing-scope pick then read it as the enclosing class's run and sent it to that class's parent — Holder::make -> Base::run in main's JavaScript: aliased constructor receivers and overridden super calls remain unresolved #2497 test, where the super is Other. A call written on super now skips the pick and takes the strategies after it, as on main.
  • main's js-class-expressions test pinned Widget::extra -> Widget::run at exact-match 0.4 (a proximity guess). this.run() inside the class expression is Widget's own member, so this branch gives the same edge at 0.9 by evidence; the test now expects that.

Full suite on 16a8e77 with the native kernel (CODEGRAPH_KERNEL_EXPECT=1): 8,922 passed, 37 skipped, 0 failed; tsc --noEmit clean.

danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 23, 2026
…per call in a class expression is not the enclosing class's member

The merge took main's colbymchenry#2831 for the conflicting Rust hunks but 167 lines of
the closed colbymchenry#2625 (rustWrittenReceiverType) auto-merged beside them and ruled
out main's own `Sink::flush` case; removed. With main's js-super-calls (colbymchenry#2818),
colbymchenry#1952's enclosing-scope pick read a class expression's `super.run()` as the
enclosing class's and sent it to its parent: a call written on `super` now
skips the pick (the same change goes to PR colbymchenry#1952). js-class-expressions pins
the 0.9 strong match colbymchenry#1952 gives `this.run()` in a class expression.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
danusha2345 and others added 3 commits October 24, 2026 16:44
A receiver-less call in Java / Scala / C++, and a `this.` / `self.` /
`super.` call in TypeScript / JavaScript / Python (which the extractor
emits under the bare name) fell through to findBestMatch whenever the
language's scoping filter left several same-named members standing, and
its same-file line-distance term handed the call to whichever one sat
nearest: `RegName.toString()`'s `render()` went to a longer sibling
class's `render`, `super.render()` to nothing or the overriding method
itself, a nested `Builder` to another `Builder`'s supertype.

matchByExactName now first walks the caller's qualified-name scopes
innermost-out: a member the caller's own class declares, then one it
inherits (resolved extends/implements edges, limited to the supertypes the
declaration's header names), then — for a bare call only — the classes it is
nested in, stopping before an outer class when an unindexed supertype could
declare the name. The receiver is read back from the call line. A match
scores 0.9; anything else keeps the existing ranking. Kotlin, C#, Swift,
Dart, Ruby and PHP are left to their own in-reach filters, which already
settle these calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y their arguments, in every language

The enclosing-scope pick returned the first same-file overload of the
caller's own member, so Scala's `this.++(1, 2)` landed on `++(o: Vec)`
where main's argument fit chooses `++(n: Int, m: Int)` (the
operator-method-names case colbymchenry#2769 added). The C++-only fall-through is
now the rule for every language: when the owner declares the name more
than once, the ranking below picks among those overloads only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ing class's member

main's js-super-calls (colbymchenry#2818) resolves `super.run()` through the class's
extends edges and declines a call it cannot place in a class member — one
in a class expression's method, which the index records from the enclosing
method. The enclosing-scope pick then read that call as the enclosing
class's `run` and sent it to its parent: `Holder::make -> Base::run`, where
the `super` is `Other` (main's colbymchenry#2497 test). A call written on `super` now
skips the pick and takes the strategies after it, as on main.

`this.run()` inside a class expression is that class's own member, so
js-class-expressions now pins the 0.9 strong match this branch gives it
instead of main's 0.4 proximity guess — the same edge, by evidence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@danusha2345
danusha2345 force-pushed the fix/bare-call-enclosing-class branch from 7776ad1 to 3016687 Compare October 24, 2026 14:04
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 24, 2026
…mchenry#2876; 11 landed PRs yield to main; EXTRACTION_VERSION 55)

Main landed eleven of our PRs as our own commits with the maintainer's additions
(colbymchenry#2847, colbymchenry#2848, colbymchenry#2850, colbymchenry#2851, colbymchenry#2853, colbymchenry#2854, colbymchenry#2855, colbymchenry#2856, colbymchenry#2868, colbymchenry#2874, colbymchenry#2875), the
traversal half of colbymchenry#2839 (colbymchenry#2860) and the trail hardening of colbymchenry#1667 (colbymchenry#2869). Where the
integration carried our original version, main's wins: play.ts, routes.ts, the
tsjs kernel extractor, import-resolver's PHP alias lookup, isDecisiveSameFileMatch,
matchNewReceiverCall and the tests of those PRs. Kept beside main: colbymchenry#1952's
enclosing-scope member pick, colbymchenry#1936's closest-copy confidence, colbymchenry#2258's Kotlin
imported-type block, colbymchenry#1875's commitHdlProfile on indexFiles and hdlAccess, colbymchenry#1583's
impact budget (the type-hierarchy walk itself is main's), colbymchenry#1736's C #if tests.
22 changelog bullets of closed PRs removed where main has its own wording.

EXTRACTION_VERSION 55: Rust receiver chains (colbymchenry#2846), `new C().m` shapes (colbymchenry#2854),
Java records (colbymchenry#2868), Go method exportedness (colbymchenry#2857), generic supertypes (colbymchenry#2856).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
colbymchenry pushed a commit that referenced this pull request Oct 28, 2026
…s member, or what it inherits

Root cause: a bare call in Java or Scala, and a `self.`/`this.` call in
Python, TypeScript and JavaScript, reaches matchByExactName as a bare name.
With several same-named methods in reach, findBestMatch ranked them by file
and line distance, so a sibling class's method declared nearer the call won
over the caller's own class's or the one it inherits; above the 500
candidate ceiling the call was dropped.

Fix (contributor): matchEnclosingScopeMember runs before the ranking. It
reads the receiver back from the call line (bare, self/this, super) and
walks the caller's scopes innermost-out: the own class, then what it
inherits, then, for a bare call, the classes it is nested in. A member found
there is the answer (0.9), and its overloads are ranked among themselves.
A super call in a JS class expression is left to js-super-calls (PR 2497).

Ours:
- Inherited members first come from the declaration's own resolved
  supertypes (getSupertypeNodes), in the order its header lists them, a
  class further up before an interface that only declares the member; the
  PR's walk by name stays as the fallback when those edges reach nothing (an
  `extends` the index resolved to a namesake, as Minestom's Player to a
  nested EntityStatuses.LivingEntity). By name alone, Django's spatialite,
  oracle and mysql GIS editors reached the postgresql backend's
  DatabaseSchemaEditor.execute and _delete_index_sql (0.9), a class with two
  bases took either one's member (BaseDeleteView(DeletionMixin, FormMixin)
  got FormMixin.get_success_url), and SystemUI's RotationContextButton got
  RotationButton's declaration instead of ButtonDispatcher.getCurrentView.
- Only `super`, `this`/`self` and Python's `cls` name the receiver. The PR
  also read `parent.`/`base.` as super and `cls.`/`static.` as self, but
  Java's extractor drops a receiver variable of those names too: protobuf's
  `parent.getFullName()` on an EnumDescriptor went to GenericDescriptor.
- C++ is left to matchCppImplicitThisCall (PR 2479), which already looks
  members up in the class, its bases and its enclosing classes. The PR's
  C++ branch added the caller's own file's namespace functions, which beat
  the overload the arguments fit: fmt's 3-argument `copy<Char>(b, e, out)`
  calls went to format.h's 2-argument copy (0.9).
- Tests for each, and the CHANGELOG bullet in the house format.

Validation: enclosing-class-member-call.test.ts has 12 cases: 6 fail
against main's resolver, and all 5 of ours fail against the PR as written. Kernel A/B against main 7cceba5 on 15 repos (leveldb, fmt, gin and
serilog byte-identical). Every changed site was judged by a hierarchy
oracle (own class, then bases depth-first in header order, classes before
interfaces, enclosing classes for a bare call) or by hand. About 7,400
links moved from a wrong method to the right one (protobuf 3,208, vscode
base+editor 1,383, Play 806, django 766, netbox 316, SystemUI 314,
spring-ai 312), and about 9,100 right links rose from 0.7 to 0.9. One site
got worse (http4s `object RespPrelude { case class RespPrelude }`), and no
C++ site changed. CPU, median of 3 interleaved runs: kestra 154.9 s on main
vs 152.8 s, vscode base+editor 79.9 s vs 82.6 s (within the rounds' spread).
On a vendored-theme stand-in of 1,500 classes, the walk links the 4,500
calls main drops at the 500-candidate ceiling, for +2.2 s (JS) and +1.8 s
(Python). Full suite on 3df1cc64 (main 31ec908): 768 files
passed, 6 skipped; 9,160 tests passed, 0 failed, 85 skipped.

Original PR #1952 by @danusha2345.

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

Copy link
Copy Markdown
Owner

Thanks @danusha2345, this landed as #2950 with you as the commit author. Resolving a call through the caller's own class before the line-distance ranking pays off widely: across 15 repos it moved about 7,400 links to the right method, among them Django test cases' self.settings(), vscode's this._register()/this.dispose() and Play's controllers, and it linked a few hundred super calls that had no link before. We made three changes on top:

  • Inherited members now come from the class's own resolved extends/implements edges, in header order, a concrete class before an interface's declaration, with your walk by name as the fallback. By name, Django's per-backend DatabaseSchemaEditors and classes with two bases took the wrong parent.
  • Only super/this/self (and Python's cls) count as the receiver, since Java variables named parent or cls reach the resolver the same way.
  • C++ is left to the C++ member lookup that main already has, because its namespace-level pick beat the overload the arguments fit (fmt's copy).

It ships in the next release.

danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 28, 2026
…mchenry#2972; 10 landed PRs yield to main; EXTRACTION_VERSION 56)

Main landed ten more of our PRs as our own commits with the maintainer's additions
(colbymchenry#2923 ← colbymchenry#1936, colbymchenry#2924 ← colbymchenry#1951, colbymchenry#2925 ← colbymchenry#1939, colbymchenry#2930 ← colbymchenry#2921, colbymchenry#2940 ← colbymchenry#1736,
colbymchenry#2942 ← colbymchenry#1947, colbymchenry#2950 ← colbymchenry#1952, colbymchenry#2961 ← colbymchenry#1954, colbymchenry#2965 ← colbymchenry#1942, colbymchenry#2972 ← colbymchenry#1933) and the
module-path half of colbymchenry#2308 (colbymchenry#2955). Where the integration carried our version, main's
wins: the enclosing-scope member pick and its tests, the Go and Java receiver typers,
the delegating-call self-loop rule, the duplicated-module copy confidence, the C #if
collapse, the module-path resolution of Rust paths, the trail/path hardening in
utils.ts. Kept beside main: colbymchenry#1875's Verilog hooks and status fields, colbymchenry#2839's
implementations command, colbymchenry#1923, colbymchenry#2922, colbymchenry#2258, colbymchenry#1761, colbymchenry#1583, the bare-call half of
colbymchenry#2308 (rust-use-bindings.ts). CHANGELOG is main's plus the open PRs' bullets.

EXTRACTION_VERSION 56: the C #if group handling (colbymchenry#2940) and the kernel change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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