Skip to content

fix(resolution): a call a method hands on to another object no longer links back to it - #2925

Merged
colbymchenry merged 1 commit into
mainfrom
claude/pr-1939-delegating-self-loop
Oct 27, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/pr-1939-delegating-self-loop

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

A method that hands its call on to another object's method of the same name, like a wrapper's def acceptsURL(url) = proxied.acceptsURL(url), no longer links that call back to itself. Before, the wrapper looked recursive in callers and impact. Recursive calls through this/self, bare calls and calls in Go keep their link.

Root cause

matchMethodCall's strategy 3 guesses a method by its name when it can't type the receiver. With only one method of that name in the project, the single-candidate branch took it outright. A delegating wrapper is often the only method of that name in the repository, because the method it delegates to belongs to a library, so the guess was the calling method itself. (The word-overlap branch already refuses the caller.)

What came from the contributor, and what we changed

From @danusha2345 (#1939): isForeignReceiverSelfLoop in the single-candidate branch (no edge when the one candidate is the caller and the receiver is not this, self, Self, super or cls), the Scala delegating-wrapper test, and the recursion test (Java this.depth() and bare size(), Go named receiver).

Ours, on top:

  1. Go is left out. The A/B showed every Go case was real recursion (8 of 8): cobra's sub.checkCommandGroups() over c.commands and cmd.Traverse(…), etcd's child.Repr(…), child.recoverAndclean() and tOp.isWrite() over op.thenOps, prometheus's subParameter.toLabels(). A Go method's own receiver and its struct fields are typed before strategy 3, so a Go call that still arrives there is on a local the resolver couldn't type, mostly a range variable over the type's own children. In every other language the rule is kept.
  2. Tests. The PR description lists a TypeScript case and a Java case the diff didn't have. The TypeScript one is real (a wrapper handing its call to an untyped parameter self-loops on main) and is added. The Java one no longer reproduces: main now gives a receiver of an unindexed or imported type no guess at all. Also added a Go range-variable recursion test, which fails on the PR as written.
  3. A CHANGELOG bullet (the PR had none), next to the VB.NET bullet about name-only guesses on untyped receivers.

Validation

Tests (__tests__/resolution.test.ts, "Name-only method match never binds a delegating call to its own caller"): 2 of 4 fail on main (the Scala and TypeScript wrappers), 1 fails on the PR as written (Go range-variable recursion), all 4 pass here.

A/B, main (3a93f57) vs this change, fresh indexes compared site by site, 24 repos in 11 languages (flask, click, fastapi, gin, cobra, etcd, prometheus, hugo, ripgrep, jq, Slim, serilog, eShopOnWeb, nowinandroid, okhttp, mihon, http4s, playframework, spring-modulith, express, zod, bulletproof-react, excalidraw, vben). Nothing is added. Every removed edge is a calls self-loop that strategy 3 had guessed at 0.7:

removed self-loops wrong (delegation) real recursion lost
PR as written 71 56 15
this change 63 56 7
  • Wrong self-loops removed (56), all by reading the source: playframework 19 (proxied.acceptsURL, underlying.withUrl, JDBC ps.executeQuery, Selenium element.click(), Java calling a Scala object's same-named method), vben 9 (form.validate() and friends in FormApi), http4s 8 (pairs.drop(n) on a Vector, segments.splitAt), mihon 8 (ClassLoader parent.loadClass, flow.collectAsState()), okhttp 5, ripgrep 3, flask 2 (jinja loader.list_templates()), spring-modulith 2. Two of playframework's were sites that also had the right edge, which they keep.
  • Real recursion through another instance of the caller's type now gets no edge (7): okhttp's Kotlin extension functions parentFile.isDescendentOf(…) and cause?.isNetworkUnusable() (3), fastapi's child_branch.effective_low_priority_routes() on a just-built _IncludedRouter (2), a serilog C# extension method (1), spring-modulith's it.containsTypeInAnyParent(…) (1). Nothing cheap tells them apart from the delegations: mihon's extension functions StateFlow.collectAsLazyPagingItems() and Preference.collectAsState() are delegations with the same shape. This is the trade-off the PR describes, measured.
  • fix(play): a Class.method reference reaches a member of the class it names #2875 changed Play's resolver after this A/B, so playframework was indexed again on that base (main at 4c572e0 vs this change on it): 17 self-loops removed (15 sites, plus 2 that keep their right edge), the same set less Java's two Multipart.multipartParser / Multipart.randomBoundary calls, which main itself now links to the Scala Multipart object (0.9). Nothing added.
  • Rebased onto current main, which since added fix(resolution): prefer the caller's own copy of a duplicated module #1936 (matchByExactName and findBestMatch) and fix(scala): resolve Outer.Inner(...) to the Scala type, never another language's constructor #1951 (Scala Outer.Inner(...) in matchMethodCall, just before strategy 3, plus a fall-through when a typed Scala match lands on another language's constructor). playframework indexed once more, main (b6379e6) vs this change on it: the same 17 self-loops removed, nothing else, and no other strategy-3 self-loop left (instance-method 0.7 self-loops 17 → 0). All 249 tests of resolution.test.ts pass, fix(scala): resolve Outer.Inner(...) to the Scala type, never another language's constructor #1951's Scala cases and the Scala wrapper case among them.
  • Kernel parity doesn't apply: TypeScript resolution only, same kernel binary in both arms.

Cost: okhttp index process CPU time, 8 interleaved rounds with the order alternated (other agents' jobs were running): median 32.8 s on main, 32.3 s with this change. The change adds one id comparison to a branch that already returns, so the difference is noise.

Suite on this commit (CODEGRAPH_KERNEL_EXPECT=1 npx vitest run --maxWorkers=4 --minWorkers=1, Windows): 760 files passed, 6 skipped; 9,041 tests passed, 83 skipped, 0 failed.

Lands #1939 by @danusha2345.

🤖 Generated with Claude Code

… links back to it

matchMethodCall's strategy 3 guesses a method by its name when the
receiver's type is unknown. With one method of that name in the project
it was taken outright, so a wrapper handing its call on, like Play's
`def acceptsURL(url) = proxied.acceptsURL(url)` or vben's
`form.validate()` inside `FormApi.validate()`, linked to the method
making the call, which then looked recursive in callers and impact.

The single-candidate branch now gives no edge when that method is the
caller itself and the receiver is another object (not this, self, Self,
super or cls), as the word-overlap branch already did.

On top of the PR: Go is left out. A Go method's receiver and struct
fields are typed before this point, so a Go call that still reaches the
guess is on a local the resolver could not type, mostly a range variable
over the type's own children, which is real recursion: cobra's
`sub.checkCommandGroups()` over `c.commands`, etcd's `child.Repr(...)`,
prometheus's `subParameter.toLabels()`. Also a TypeScript wrapper test, a
Go range-variable recursion test, and a CHANGELOG bullet.

Validation, main (3a93f57) vs this change, 24 repos in 11 languages: no
edge added; 63 self-loop edges removed, 56 of them wrong (delegations to
a library or another object: playframework 19, vben 9, http4s 8, mihon 8,
okhttp 5, ripgrep 3, flask 2, spring-modulith 2) and 7 real recursion
through another instance of the caller's type, which now gets no edge
(okhttp's Kotlin extension functions on File and Throwable 3, fastapi's
_IncludedRouter 2, a serilog C# extension method 1, spring-modulith 1).
As written the PR also removed the 8 Go recursion edges. Re-run on
playframework after #2875 changed Play's resolver: 17 removed, the same
less Java's two Multipart.* calls, which main now links to the Scala
object itself. Index CPU on okhttp, median of 8 interleaved runs: 32.8 s
on main, 32.3 s with this change. Tests: 2 of 4 fail on main; the PR as
written fails the Go range-variable case.

Original PR #1939 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 0b19444 into main Oct 27, 2026
@colbymchenry
colbymchenry deleted the claude/pr-1939-delegating-self-loop branch October 27, 2026 16:45
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