Skip to content

fix(resolution): prefer the caller's own copy of a duplicated module - #1936

Closed
danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:fix/prefer-same-module-candidate
Closed

danusha2345 wants to merge 1 commit into
colbymchenry:mainfrom
danusha2345:fix/prefer-same-module-candidate

Conversation

@danusha2345

Copy link
Copy Markdown
Contributor

Problem

Some repos keep near-identical copies of a module under several source roots, for example two app flavours: module/rc2/app/src/... and module/rcpro2/app/src/.... Every symbol then has one same-named candidate per copy. findBestMatch already scores directory proximity, so on shallow trees the caller's own copy wins. The result has two flaws, though:

  • Wrong target on deep trees. Proximity is capped (15 per shared folder, max 80). When two copies both share 6+ folders with the caller, they tie, and the first-indexed copy wins, which can be the other module's.
  • Understated confidence. A correct pick is reported at 0.7, or 0.4 on shallow trees, as if the name were ambiguous. Only which copy is ambiguous; the name is not.

Change

In findBestMatch, only among candidates that already tie on the full existing score, and only when every candidate is a copy of one symbol (same qualified name, kind and file basename), the copy with the longest uncapped shared directory prefix with the caller wins. It must be uniquely closest; otherwise nothing changes.

  • Precedence. Same file, imports, lexical scope, kind and language all keep their priority; a single candidate is never touched.
  • Confidence. When the pick is that unique closest copy, it gets 0.8: above a proximity guess (0.7), below a unique name (0.9).

Measured

I indexed 12 real projects (Kotlin ×3, C, Rust ×2, Go, Python ×2, PHP, C#, Scala) with main and with this branch. Node counts are identical everywhere.

project edges changed exact-match edges now at 0.8
Kotlin app with two module copies 0 2,006 (its 0.7 exact-match call edges: 1,236 → 260)
Rust app 2 → 2 58
playframework 1 → 1 184
others 0 0–389

Changed edges, checked against source:

  • Rust: singbox/detect.rs::locate constructs the Located of its own file. The old edge went to mihomo/detect.rs::Located.
  • playframework: a test in shutdown-happy-path/src/test now instantiates its own src/main HomeController rather than another copy's.

0.8 edges: a script checked all 2,719 new 0.8 edges in the six projects that have any. Each points at the strictly closest copy, with no exceptions.

Side effect: a 0.8 name match now outranks framework resolutions scored 0.7–0.75. In these projects that happened once (the Rust case above), and it fixed a wrong edge.

Tests

New __tests__/same-module-copy-resolution.test.ts (Python, deep paths so the cap ties) covers three cases:

  • each copy's caller resolves into its own copy at 0.8; this fails on main on the targets themselves;
  • an equidistant caller keeps the old choice at 0.7;
  • a single candidate stays at 0.9.

The full suite passes (4721), and tsc / build:kernel are clean.

🤖 Generated with Claude Code

@danusha2345
danusha2345 force-pushed the fix/prefer-same-module-candidate branch 2 times, most recently from d163409 to 1bcd944 Compare September 30, 2026 18:54
@danusha2345
danusha2345 force-pushed the fix/prefer-same-module-candidate branch 4 times, most recently from 9792f63 to 7348a2b Compare October 10, 2026 11:54
@danusha2345
danusha2345 force-pushed the fix/prefer-same-module-candidate branch 6 times, most recently from 9997fb3 to 32fb09e Compare October 22, 2026 10:50
When a bare name matches several copies of the same symbol (same qualified
name, kind and file basename) in sibling modules, capped directory proximity
can tie them on deep trees and the first-indexed copy won. Break such ties
toward the copy sharing the longest directory prefix with the caller, and
only when it is uniquely closest; otherwise the prior choice stands.

A match picked as the caller's own copy of one symbol gets confidence 0.8
(the name is unambiguous, only the copy is), between the 0.7 proximity
guess and the 0.9 unique-name match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@danusha2345
danusha2345 force-pushed the fix/prefer-same-module-candidate branch from 32fb09e to 5abdf25 Compare October 24, 2026 14:05
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 27, 2026
… 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

Copy link
Copy Markdown
Owner

Thank you, @danusha2345! This landed as #2923 with you as the commit author: a call now reaches the caller's own copy of a duplicated module (at 0.8), and on spring-modulith's example apps it fixed three links that Spring's naming rule had sent to another example's copy. Your rebase onto main, which carries #1938 as #2874, resolved the shared confidence line just as we had. On top, we judge the copies among same-language candidates, keep a match of the caller itself at the proximity score, and added Kotlin and Java flavor tests, since the Python fixture calls compute() without importing it. It ships in the next release.

colbymchenry pushed a commit that referenced this pull request Oct 27, 2026
…object it builds

Scala qualified names carry no package (Outer::Inner), so a call written
Outer.Inner(args), a case-class or companion apply, found no type by its
written name. It fell through to the name strategies, and under a return
type annotation (def f(): WebSocket = WebSocket.Accepted(flow)) the
local-declaration reader took WebSocket for the receiver's type and handed
over an unrelated Java constructor, Other::Accepted::Accepted.

Before the receiver-word strategy, a Scala call whose member is
capitalized now resolves to the Scala type Outer::Inner when that is
unambiguous: every match in one file (a class and its companion), or one
in the call's own file; a class or trait wins over a same-named object.
It is recorded as instantiates for a class, as a call for an object. The
typed-receiver path no longer hands a Scala call another language's
constructor. Real Java static methods are still reached from Scala.

Validation:
- resolution.test.ts: 5 cases, 4 red on main; the fifth (a Scala call
  to a real Java static) passes on both and guards the Java side.
- A/B against main 81559dc (with #1950 and #1936): scalachess +77/-0,
  http4s +371/-1 (the one a retarget from a wrong defaults::Host to the
  Origin.Host case class), playframework +291/-1 (the one a retarget:
  Writeable's HttpEntity.Strict(...) from the Java play.http constructor
  to the Scala case class), play-samples +2 (two case-class applies #1950
  unlinked from Java constructors); CMAK, click and gin identical. Every
  addition is a Scala case-class or object apply; sampled all right
  (Uci.Move, Header.Raw, H2Frame.Data, RemoteNode.Ip,
  ApplicationLoader.Context, Request.Connection over headers.Connection).
- Cost (http4s, median of 3 interleaved fresh indexes, on main
  69af19a): wall 8.6 s on main vs 7.8 s, process CPU 27.5 s vs
  26.2 s, within noise. The A/B gave the same edges on both bases.

Original PR #1951 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.

2 participants