Skip to content

fix(resolution): a call on the caller's own class reaches that class's member, or what it inherits - #2950

Merged
colbymchenry merged 1 commit into
mainfrom
claude/pr-1952-enclosing-class-member
Oct 28, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/pr-1952-enclosing-class-member

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

Lands #1952 by @danusha2345. These calls now link to the method of the class they're written in, or to the one that class inherits:

  • in Java and Scala, a call with no receiver;
  • in Python, TypeScript and JavaScript, a call on self or this.

Before, they could link to a same-named method of another class that is declared closer to the call. In Java and Scala, a call inside a nested class also reaches the enclosing class's method, and a super call in Java or Python reaches the parent's method.

Root cause

The extractor records render(), this.render(), self.render() and super.render() all under the bare name render, so they reach matchByExactName.

  • When several methods of that name are in reach, findBestMatch ranks them by file and line distance, so a sibling class's render declared next to the call beat the caller's own class's, or the one it inherits.
  • Above the 500-candidate ceiling, the call was dropped.

From the contributor, and what we changed

  • Contributor: matchEnclosingScopeMember, which runs before the ranking.
    • It reads the receiver back from the call's 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 enclosing classes.
    • The member it finds is the answer at 0.9, and that member's overloads are ranked among themselves.
    • It leaves a super call in a JS class expression to js-super-calls (JavaScript: aliased constructor receivers and overridden super calls remain unresolved #2497).
    • It comes with __tests__/enclosing-class-member-call.test.ts.
  • Ours:
    1. Inherited members through the class's own resolved supertypes.
      • The walk now follows the declaration's own extends/implements edges (getSupertypeNodes), in the order its header names them, and takes a class further up over an interface that only declares the member.
      • The PR's walk by name stays as the fallback when those edges reach nothing. That covers an extends the index resolved to a namesake: Minestom's Player extends LivingEntity points at a nested EntityStatuses.LivingEntity, and Play's doc controllers point at a doc-local BaseController.
      • With the walk by name alone:
        • Django's spatialite, oracle and mysql GIS schema editors reached the postgresql backend's DatabaseSchemaEditor.execute and _delete_index_sql at 0.9. Django has one DatabaseSchemaEditor per backend, and each of these editors inherits both methods from BaseDatabaseSchemaEditor through its own backend's class.
        • A class with two bases took an arbitrary base's member: BaseDeleteView(DeletionMixin, FormMixin) got FormMixin.get_success_url, and AdminPasswordChangeForm(SetUnusablePasswordMixin, SetPasswordMixin, …) got SetPasswordMixin's methods.
        • SystemUI's RotationContextButton extends ContextualButton implements RotationButton got RotationButton's declaration of getCurrentView instead of ButtonDispatcher's implementation.
    2. Only super, this/self and Python's cls name the receiver. The PR also read parent./base. as super and cls./static./Self./$this as self. Java's extractor drops a receiver variable of those names too, so protobuf's parent.getFullName() on an EnumDescriptor went to GenericDescriptor at 0.9.
    3. C++ is left to matchCppImplicitThisCall (fix(cpp): a call a method makes with no receiver links to its class's own or inherited method #2479). That function already finds members in the class, its bases and its enclosing classes. The PR's C++ branch added the caller's own file's free functions at namespace scope, and those beat the overload the arguments fit: fmt's 3-argument copy<Char>(begin, end, out) calls went to format.h's 2-argument copy(string_view, out) at 0.9. It also turned constructor calls into instantiates edges. Some of its namespace picks were right, so C++ namespace lookup with overload fitting would make a good follow-up.
    4. Tests for each of these, and the CHANGELOG bullet in the house format.

Our part of the change is 86 of the 290 lines of the new walk and its helpers, about 30%. We replaced 15 of the PR's lines, all of them C++ handling, the receiver words, the base order and comments.

Validation

  • Red to green:
    • enclosing-class-member-call.test.ts has 12 cases. Against main's resolver (31ec908), 6 of them fail: the PR's Scala, Python self., TypeScript this., Java super. and Java nested Builder cases, plus the first-base case.
    • Against the PR as written, all 5 of our new cases fail: namesake base, first base, concrete class before interface, parent variable, and C++ namespace overload.
    • js-class-expressions, js-super-calls and super-call-self-edge pass unchanged. The current head doesn't touch the js-class-expressions expectations, so there is no 0.4 → 0.9 change to justify.
  • Full suite on 3df1cc64 (main 31ec908), kernel built from source, CODEGRAPH_KERNEL_EXPECT=1: 768 files passed, 6 skipped; 9,160 tests passed, 0 failed, 85 skipped. Rebased onto 6162d87, which changes only src/sync/watcher.ts (fix(watcher): a deleted link target no longer pins a CPU core on Windows #2938). The patch-id is unchanged, and the PR's test passes there.
  • A/B, kernel arms, main 7cceba5 (fix(resolution): library calls on untyped Python, Scala and Java values stay unlinked #2942's library lists included) against this branch:
    • The change touches resolution only, so no kernel parity run is needed.
    • Every changed site was judged by an independent oracle. It reads the receiver from the source line and walks the graph's own contains and extends/implements edges: the caller's class, then its bases depth-first in header order, classes before interfaces, then the enclosing classes for a bare call.
    • Sites the oracle couldn't judge were read by hand.
Repo Language Edges, main → branch Wrong → right Right → wrong Same target, 0.7 → 0.9 Read by hand
flask Python 5,288 → 5,288 1 0 7
fastapi Python 22,326 → 22,327 2 0 14
netbox Python 77,636 → 77,636 316 0 518 3 cls. calls, right
django Python 193,809 → 193,809 766 0 1,503 6: 2 cls. right, 4 lookups through a class defined in a test method, plausible
excalidraw TypeScript 60,663 → 60,663 2 0 15
vscode base + editor TypeScript 200,924 → 200,925 1,383 0 975
scalachess Scala 16,586 → 16,586 18 0 28 5 enum-case confidence, right
http4s Scala 41,435 → 41,435 56 1 113 2
playframework Scala, Java 98,971 → 99,036 806 0 393 27 doc controllers' Action, right
Minestom Java 128,852 → 128,903 66 0 127 17, all right (15 super. calls)
spring-ai Java 168,525 → 168,576 312 0 282
kestra Java 264,175 → 264,244 90 0 290
SystemUI src (AOSP) Java 108,683 → 108,982 314 0 33 1
protobuf Java, Python, C++ 390,468 → 390,516 3,208 0 4,256
rocksdb C++ (its Java, Python) 319,848 → 319,850 8 0 568
leveldb, fmt C++ identical
gin, serilog (controls) Go, C# identical

About 7,400 links now go to the right method, and about 9,100 right links went from 0.7 to 0.9.

  • The added edges are mostly super. calls, which main left unresolved: SystemUI 299, kestra 59, spring-ai 51, protobuf 48, Play 35.
  • The large groups include:
    • Django test cases' self.settings() reaching SimpleTestCase.settings instead of a mail handler's;
    • vscode's this._register()/this.dispose() reaching Disposable's;
    • spring-ai builders' self() reaching DefaultChatOptionsBuilder.self;
    • protobuf's generated Java builders reaching their own members.
  • One site got worse: in http4s, object RespPrelude { case class RespPrelude … RespPrelude(…) } now reaches the object rather than the case class. A bare name equal to its scope is read as a constructor.
  • No C++ site changed. Neither did any edge fix(resolution): library calls on untyped Python, Scala and Java values stay unlinked #2942 removed: the library lists act on calls with a receiver, this change on bare names, so they compose. The scalachess, http4s, Play and Minestom deltas are the same on 0174f80 as on 7cceba5.

Against the PR as written (on 0174f80), the branch keeps every Java, Scala, Python and TypeScript improvement. It also fixes:

  • 18 Django sites that went from one wrong target to another, 3 that went from right to wrong, and 3 confidence raises on a wrong target;
  • 2 SystemUI sites that went from right to wrong (the interface declaration);
  • protobuf's parent. sites.

The PR's C++ changes now stay as main has them. That is fmt's 33 retargets and 5 calls → instantiates sites, including the wrong copy/write overloads, and protobuf's 101 C++ retargets and 39 calls → instantiates sites. Leaving C++ out also gives up that branch's right picks: fmt's 6 calls through a local named self, and protobuf pyext's namespace-scoped _GetDescriptor calls.

The name fallback keeps the PR's right answers where the index resolved an extends to a namesake (Minestom's 15 super. calls, Play's 26 doc Action calls). Both of those extends resolutions are bugs of their own, worth a separate fix.

  • Cost, median of 3 interleaved runs, CPU seconds:
Repo Main Branch
kestra (Java, about 5,000 files) 154.9 s 152.8 s
vscode base + editor (TypeScript) 79.9 s 82.6 s (rounds 79.1-82.8 on main, 75.8-83.3 here)
stress, JS (1,500 widget classes) 12.2 s 14.4 s
stress, Python 11.3 s 13.1 s
stress, Java 28.3 s 27.4 s

The walk runs before the 500-candidate ceiling, so a call on a ubiquitous name pays one pass over its candidates. The stress fixtures are a stand-in for a vendored theme like Metronic (#999): 1,500 widget classes that share init/update/render/destroy and call each other through this./self./a bare call. There, the walk links the 4,500 calls main drops at the ceiling, all to the caller's own class.

Lands #1952 by @danusha2345.

🤖 Generated with Claude Code

…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
colbymchenry merged commit 2577601 into main Oct 28, 2026
@colbymchenry
colbymchenry deleted the claude/pr-1952-enclosing-class-member branch October 28, 2026 00:11
colbymchenry pushed a commit that referenced this pull request Oct 28, 2026
…rary type gets no guess

Java typed a call receiver from a local declaration or a plain field of
the enclosing class only. A field of an outer class read inside an inner
or anonymous class (mHandler.post(...) inside new Runnable() {...}),
Outer.this.f, a chain of fields (this.mOwner.mRepo.save()) and a local
whose type has type arguments fell through to the name-only strategies,
and so did every receiver whose declared type is a library class:
parcel.readInt() on an android.os.Parcel went to a project
VersionedParcel.readInt, list.size() to any project size().

The declared type is now read for each of those shapes, a chain one hop
at a time with each hop's own imports, keeping a written package or
outer class (Map.Entry, play.api.mvc.BodyParser). A receiver whose
declared type the project does not declare, or that an import or a
written package pins outside the project, gets no edge. When no import
decides between same-named types, a Java call prefers the Java
declaration over a Scala or Kotlin one.

Also, from review: a field the class around the call declares is read
as written before the member reader, which keeps only the last name
(PauseTest.Suite, a field written with its package); a local or loop
variable keeps the package its declaration writes; a field typed by a
type parameter goes to its bound, and a class the class head names as
a type argument (Comparable<ApplicationModule>) is not taken for one; a
nested type a superclass declares, or one an on-demand import of its
class names (import play.mvc.Http.*), counts as named; and a typed miss
on a type no project class or interface is named (a URI, an enum's
name()) gets no guess, as on main.

Validation:
- java-receiver-type.test.ts: 19 tests, the PR's 5 red on main; the
  other 14 pass on main, and our 8 each fail without their change.
- A/B against main 2577601 (with #2942 and #2950; the same sites
  change against 7cceba5), sites added/removed/retargeted: jextract
  +2/-1/106, spring-modulith +1/-3/2, Minestom +52/-91/71, SystemUI
  +53/-319/96, spring-ai +69/-659/532, kestra-java +5/-504/162,
  playframework +0/-196/599; click and gin identical. Of the removed
  edges main had at 0.9, 1,004 were wrong (an import oracle plus 16
  read by hand) and 4 right (a lambda parameter, two nested types of a
  Scala object, a field read under a static import of a same-named
  member). Retargets land on the declared type or the supertype
  declaring the method, except about 16 build()/clone() calls on
  spring-ai's nested option builders, which move from one wrong target
  to another.
- Cost (3 interleaved rounds): spring-ai wall 27.9 s on main vs
  25.9 s, CPU 90.2 s vs 88.1 s. SystemUI read 71.3 vs 79.4 s wall
  under other jobs' load; CPU profiles of both arms put the new code at
  about 0.6 s of 115 s.

Original PR #1942 by @danusha2345.

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

1 participant