Skip to content

fix(resolution): a call reaches the caller's own copy of a duplicated module - #2923

Merged
colbymchenry merged 1 commit into
mainfrom
claude/pr-1936-closest-copy
Oct 27, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/pr-1936-closest-copy

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

When a project keeps copies of the same code in several folders, like Android build flavors that each hold one package, or several example apps that each carry the same packages, a call now reaches the copy in the caller's own folder. In a deep tree it used to take whichever copy was indexed first, and the right pick was understated as a proximity guess.

Root cause

findBestMatch scores directory proximity at 15 points per shared leading folder, capped at 80, so two copies that both share six or more folders with the caller tie, and the first one indexed wins. In matchByExactName the winner's confidence came from proximity alone (0.7, or 0.4 in a shallow tree), though among copies of one symbol the name is never in doubt, only which copy.

Reproduced on main with a realistic layout: two Kotlin flavor source sets platform/apps/mobile/android/app/src/rc2/… and …/src/rcpro2/… holding one package. Both flavors' run() linked to rc2's compute(), and rcpro2's new Util() in Java to rc2's class.

What came from the contributor, and what we changed

From @danusha2345 (#1936): closestCopy and sharedDirDepth, the tie-break in findBestMatch (only among candidates that tie on the full score, and only when every one is a copy: same qualified name, kind and file name), the 0.8 confidence, the Python tests, and the CHANGELOG bullet.

Ours, on top:

  1. Rebased onto fix(resolution): score a decisive same-file match as strong #1938 (merged as fix(resolution): a lone same-file definition scores as a strong match #2874), which edits the same confidence line. Resolved to keep both: the file's own decisive definition 0.9 first, then the caller's own copy 0.8, then proximity. The contributor's later force-push (5abdf25, onto current main) resolves that line the same way and is otherwise the patch we cherry-picked, so nothing more needed folding in.
  2. Copies are judged among the candidates findBestMatch weighed. It skips other languages whenever a same-language candidate exists, so the tie-break already ignored them, but the 0.8 check didn't: a Python compute in the same repo kept two Kotlin copies at 0.7. In playframework this raised 13 more right picks (each sample project's own models.User and Application methods).
  3. A match of the caller itself keeps the proximity score, as fix(resolution): score a decisive same-file match as strong #1938 does for a same-file match: recursion can't be told apart from a call that lost its receiver.
  4. Realistic tests: the PR's Python fixture calls compute() without importing it, which Python can't do. Added Kotlin flavor copies (bare call in one package) and Java flavor copies (new Util()), plus the self-match case.
  5. CHANGELOG: moved next to fix(resolution): score a decisive same-file match as strong #1938's bullet and reworded, with Thanks @danusha2345.

Validation

Tests. __tests__/same-module-copy-resolution.test.ts, 6 cases: 3 fail on main on the targets themselves (Python, Kotlin and Java copies), 2 fail on the PR as written, re-checked on its current head 5abdf25 (the Kotlin confidence and the self-match), all 6 pass here.

A/B, main (with #2874) vs this change, fresh indexes, site by site, 21 repos in 11 languages (flask, click, fastapi, gin, cobra, etcd, ripgrep, jq, Slim, serilog, eShopOnWeb, nowinandroid, okhttp, http4s, playframework, spring-modulith, express, zod, bulletproof-react, excalidraw, vben):

repo edges added removed retargeted confidence only via a framework rule before
spring-modulith 10,048 0 0 3 15 14 (+3 retargeted)
playframework 61,076 0 0 1 38 0
http4s 25,631 0 0 0 21 0
etcd 42,643 0 0 0 21 1
vben 20,184 0 0 0 10 0
bulletproof-react 4,420 0 0 0 3 0
okhttp 31,980 0 0 0 1 0
the other 14 130,768 0 0 0 0 0
total 326,750 0 0 4 109 15 (+3)

Triage:

  • The 4 retargets are right. spring-modulith's spring-modulith-example-full tests now reach that example's own OrderManagement and OrderCompleted, where Spring's naming rule (0.7) had taken the epr-jdbc example's copy (3). playframework's shutdown-happy-path scripted test now instantiates its own src/main HomeController, not the changes/ copy its script swaps in later (1, the case the PR describes).
  • The 109 confidence raises keep their target, each onto the copy in the caller's own module or app: every spring-modulith example's own classes, http4s's client tests reaching the client middleware and server tests the server's, each vben app's main.ts reaching its own bootstrap.ts, each bulletproof-react app's mocks reaching its own db.ts, etcd's pkg/proxy test reaching pkg/proxy. In Go, qualified names carry no package, so same-named functions in same-named files of different packages count as copies; the own-package pick is what a bare Go call means anyway. 4 of the 109 are wrong targets main already had at 0.7: vben's defineConfig calls from import { defineConfig } from 'tsdown' link to the folder's own src/index.ts namesake, because tsdown is declared only in the root package.json (a pnpm catalog: entry) and so isn't recognized as an outside package from a workspace package. That gap predates this change.
  • The 0.8 against framework rules (0.7–0.75): in 18 sites it now beats one (spring-modulith 17, etcd 1); 3 are the retargets above, the rest pick the same target.
  • After rebasing onto fix(play): a Class.method reference reaches a member of the class it names #2875 (Play's resolver), playframework was indexed again with the new main and this branch: the same 1 retarget and 38 raises, nothing else.
  • Kernel parity doesn't apply: TypeScript resolution only, same kernel binary in both arms.

Cost, index process CPU time, interleaved, median of 3: okhttp 34.8 s vs 34.8 s, etcd 36.5 s vs 36.5 s.

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

Lands #1936 by @danusha2345.

🤖 Generated with Claude Code

… module

A module kept in several copies, like two Android build flavors that each
hold one package, gives every name in it one candidate per copy.
Directory proximity is capped at six shared folders, so in a deep tree
the copies scored the same and the first indexed won: a call in one
flavor could land on the other flavor's copy. And since the name is not
in doubt among copies, only which copy, scoring the right pick by
proximity (0.7, or 0.4 in a shallow tree) understated it.

findBestMatch now breaks a tie among copies of one symbol (same
qualified name, kind and file name) toward the copy whose file shares
the longest directory prefix with the reference, when that copy is
uniquely closest. matchByExactName scores that pick 0.8: above a
proximity guess, below a unique name or the file's own definition (0.9,
which keeps precedence).

On top of the PR: the copies are judged among the candidates
findBestMatch weighed, so a namesake in another language no longer
blocks the 0.8; a match of the caller itself keeps the proximity score,
as it does for a same-file match; and Kotlin and Java flavor tests, since
the PR's Python fixture calls a function it never imports.

Validation, main (cde4cd7) vs this commit, 21 repos in 11 languages:
no edge added or removed; 4 targets moved, all right: spring-modulith's
example-full tests now reach example-full's own OrderManagement and
OrderCompleted, where Spring's naming rule (0.7) took the epr-jdbc
example's copy, and playframework's shutdown-happy-path test its own
src/main HomeController, not the copy its script swaps in later. 109
edges keep their target and go to 0.8, each onto the copy in the
caller's own module or app; 4 of them are vben's `defineConfig` calls
from `import { defineConfig } from 'tsdown'`, which main already links
to a namesake because that dependency is declared only in the root
package.json. In 18 sites the 0.8 now beats a framework rule's 0.7.
Index CPU, median of 3 interleaved: okhttp 34.8 vs 34.8 s, etcd 36.5 vs
36.5 s. Tests: 3 of 6 fail on main, 2 on the PR as written.

Original PR #1936 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 81559dc into main Oct 27, 2026
@colbymchenry
colbymchenry deleted the claude/pr-1936-closest-copy branch October 27, 2026 15:26
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