fix(resolution): a lone same-file definition scores as a strong match

matchByExactName scored a winner among several same-named candidates by
directory proximity alone: 0.7 when the files share two folders, else
0.4. findBestMatch already ranks the definition in the reference's own
file first, so the target was right, but a shallow file's own function
read as an uncertain guess (below 0.6), and because the resolver keeps
the most confident strategy, the same-file definition lost to a
framework's naming guess in another file (ASP.NET's PascalCase and
Service rules at 0.7 and 0.85).

isDecisiveSameFileMatch now scores it 0.9, as a lone candidate, when it
is the only candidate in the file, is not the caller itself, and, as a
member of a type, module or namespace, belongs to one the caller is in.
The PR checked that last rule for methods only. We extended it to every
member kind whose owner the file declares, other than a function, so a
sibling enum's entry or a sibling class's field is not raised.

Validation, main vs this commit, 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):
- no edge added or removed;
- 9 targets moved, all in eShopOnWeb and all right: C# properties and
  fields named after their type that the code reads (Color Color), which
  ASP.NET's naming rule had linked to the class in another project;
- 6,927 edges keep their target and gain confidence, 535 of them from
  0.4; in 874 a framework rule's 0.7-0.85 now loses to the same target by
  exact name; 30 end in another file, Go type positions the type gate
  moves to the package's own type;
- as written, the PR also raised 77 edges onto another type's member to
  0.9, about 48 of them wrong (okhttp's InterceptorOverridesTest enum
  entries, playframework's sibling providers' `get`); they keep main's
  confidence.
Index CPU, median of interleaved runs: okhttp 32.3 vs 32.4 s, etcd 33.4
vs 33.1 s. Tests: 3 of 7 fail on main; the PR as written fails the
sibling-member case. js-class-expressions pinned the old 0.4 for
`this.run()` reaching the class's own `run` in a shallow file; it now
expects 0.9.

Original PR #1938 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
danusha2345
2026-10-24 01:23:32 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent 0b238452a3
commit cde4cd74eb
4 changed files with 177 additions and 2 deletions
+52 -1
View File
@@ -12049,7 +12049,9 @@ export function matchByExactName(
if (bestMatch && isCrossFileReachable(bestMatch, ref, context)) {
// Lower confidence when the match is from a distant/unrelated module
const proximity = computePathProximity(ref.filePath, bestMatch.filePath);
const confidence = proximity >= 30 ? 0.7 : 0.4;
const confidence = isDecisiveSameFileMatch(bestMatch, ref, candidates, context)
? 0.9
: proximity >= 30 ? 0.7 : 0.4;
return {
original: ref,
targetNodeId: bestMatch.id,
@@ -26730,6 +26732,55 @@ function computePathProximity(filePath1: string, filePath2: string): number {
return pathProximityFromDirs(dir1, filePath2);
}
/**
* Whether `findBestMatch` picked `best` because it is THE definition in the
* call site's own file, so the edge deserves a same-file confidence rather than
* the directory-proximity one. Proximity measures shared folders, so a
* same-file match in a shallow path (`scripts/x.py` → its own `main`) scored
* 0.4 like a guess across unrelated modules. Decisive only when:
* - `best` is the ONLY same-file candidate — several (two classes' `run` in
* one file) are separated by line distance, which is not evidence; and
* - a member of a type, module or namespace belongs to one the caller is
* inside — a bare call does not reach a sibling class's method just
* because it is written in that file, nor is okhttp's `Dns { … }` in one
* nested object the `Dns` entry of the enum beside it. A method's owner is
* always such a scope; another kind's is judged when the file declares it
* as anything but a function, whose nested definitions lexical
* reachability already scoped; and
* - `best` is not the caller itself — `super.onCreate()`,
* `parent::__construct()` and `json.dumps` inside `dumps` all arrive here
* without their receiver, and recursion cannot be told apart from them.
* 0.9 matches the unique exact-match above: the same-file bonus in
* findBestMatch outweighs any directory proximity, so a lone same-file
* definition is as settled as a lone candidate. The resolver keeps the most
* confident strategy, so this also outranks a sub-0.9 framework guess that
* sent the name to another file (ASP.NET's naming rule took eShopOnWeb's
* `ToastService.OnShow` for the `ToastService` class, not the component's
* property of that name) — the file's own definition is the one the name
* means.
*/
function isDecisiveSameFileMatch(
best: Node,
ref: UnresolvedRef,
candidates: Node[],
context: ResolutionContext
): boolean {
if (best.filePath !== ref.filePath || best.id === ref.fromNodeId) return false;
if (candidates.some((c) => c !== best && c.filePath === ref.filePath)) return false;
const sep = best.qualifiedName.lastIndexOf('::');
if (sep <= 0) return best.kind !== 'method';
const owner = best.qualifiedName.slice(0, sep);
if (best.kind !== 'method') {
const cut = owner.lastIndexOf('::');
const ownerName = cut >= 0 ? owner.slice(cut + 2) : owner;
const declared = (context.getNodesInFileNamed?.(best.filePath, ownerName) ?? context.getNodesInFile(best.filePath))
.filter((n) => n.qualifiedName === owner);
if (declared.every((n) => n.kind === 'function' || n.kind === 'method')) return true;
}
const from = context.getNodeById?.(ref.fromNodeId);
return !!from && (from.qualifiedName === owner || from.qualifiedName.startsWith(`${owner}::`));
}
/**
* Find the best matching node when there are multiple candidates
*/