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
+1 -1
View File
@@ -175,7 +175,7 @@ describe.each(['js', 'ts'])('a class expression\'s name reaches it from inside a
expect(edgesFrom(graph, `src/widgets.${ext}`)).toEqual([
'calls Panel::PanelImpl::go -> Base::run [instance-method 0.9]',
'calls Widget::extra -> Widget::run [exact-match 0.4]',
'calls Widget::extra -> Widget::run [exact-match 0.9]',
'calls Widget::run -> Base::run [instance-method 0.9]',
`calls src/widgets.${ext} -> register [import 0.9]`,
'extends Panel::PanelImpl -> Base [import 0.9]',
@@ -0,0 +1,123 @@
/**
* Confidence of an exact-name match the call site's own file decided.
*
* With several same-named candidates, the name-matcher ranks the one in the
* call site's file first, then scored the winner by DIRECTORY proximity — the
* number of leading folders its path shares with the caller's. A same-file
* winner shares every folder, but a shallow file has few to share, so
* `scripts/x.py` calling its own `main` got 0.4, the score of a guess across
* unrelated modules. A lone same-file definition now scores like a lone
* candidate; the matcher's pick itself never changes, and a pick the file
* does not settle (two same-file candidates, a sibling class's method, the
* caller itself) keeps the proximity score.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { CodeGraph } from '../src';
describe('same-file exact-match confidence', () => {
let dir: string;
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'same-file-conf-')); });
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
const write = (rel: string, body: string) => {
const p = path.join(dir, rel);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, body);
};
type CallEdge = { src: string; tgt: string; tgtQn: string; tgtFile: string; confidence: number };
const calls = async (kind = 'calls'): Promise<CallEdge[]> => {
const cg = await CodeGraph.init(dir, { silent: true });
await cg.indexAll();
const db = (cg as any).db.db;
const rows: CallEdge[] = db
.prepare(
`SELECT s.name src, t.name tgt, t.qualified_name tgtQn, t.file_path tgtFile,
json_extract(e.metadata, '$.confidence') confidence
FROM edges e
JOIN nodes s ON s.id = e.source
JOIN nodes t ON t.id = e.target
WHERE e.kind = ?`
)
.all(kind);
cg.close?.();
return rows;
};
it('scores a shallow-path call to its own file as a strong match', async () => {
write('x.py', 'def helper():\n return 1\n\ndef main():\n return helper()\n');
write('pkg/other.py', 'def helper():\n return 2\n');
const edge = (await calls()).find((e) => e.src === 'main' && e.tgt === 'helper');
expect(edge?.tgtFile).toBe('x.py');
expect(edge?.confidence).toBe(0.9);
});
it('keeps the same-file target when a sibling file defines the name too', async () => {
write('a/b/c/x.py', 'def helper():\n return 1\n\ndef main():\n return helper()\n');
write('a/b/c/y.py', 'def helper():\n return 2\n');
const edge = (await calls()).find((e) => e.src === 'main' && e.tgt === 'helper');
expect(edge?.tgtFile).toBe('a/b/c/x.py');
expect(edge?.confidence).toBe(0.9);
});
it('leaves the proximity score when the file holds several candidates', async () => {
// Both branches of an `#ifdef` define `helper`: line distance picks one.
write(
'x.c',
'#ifdef FAST\nstatic int helper(void) { return 1; }\n#else\nstatic int helper(void) { return 2; }\n#endif\n' +
'int main(void) { return helper(); }\n'
);
write('lib/other.c', 'int helper(void) { return 3; }\n');
const edge = (await calls()).find((e) => e.src === 'main' && e.tgt === 'helper');
expect(edge?.tgtFile).toBe('x.c');
expect(edge?.confidence).toBe(0.4);
});
it('leaves the proximity score when the match is the caller itself', async () => {
// Recursion, or a receiver the call lost (`json.dumps` inside `dumps`).
write('x.py', 'def walk(n):\n return walk(n - 1)\n');
write('pkg/other.py', 'def walk(n):\n return 2\n');
const edge = (await calls()).find((e) => e.src === 'walk' && e.tgt === 'walk');
expect(edge?.tgtFile).toBe('x.py');
expect(edge?.confidence).toBe(0.4);
});
it("leaves the proximity score for a sibling class's method", async () => {
// The other `run` is one the file includes: C++ sees no other file's (#2684).
write('x.cpp', '#include "pkg/other.h"\nstruct A {\n int run() { return 1; }\n};\nint go() { return run(); }\n');
write('pkg/other.h', 'inline int run() { return 2; }\n');
const edge = (await calls()).find((e) => e.src === 'go' && e.tgt === 'run');
expect(edge?.tgtQn).toBe('A::run');
expect(edge?.confidence).toBe(0.4);
});
it("outranks a framework's naming guess that sends the name to another file", async () => {
// ASP.NET's naming rule takes `ToastService` for the class in Services/,
// but the component reads its own property named after that class, as
// eShopOnWeb's Blazor ToastComponent does (C#'s "Color Color").
write('Startup.cs', 'namespace App { public class Startup { } }\n');
write('Services/ToastService.cs', 'namespace App.Services {\n public class ToastService { public event System.Action OnShow; }\n}\n');
write(
'Helpers/ToastComponent.cs',
'using App.Services;\nnamespace App.Helpers {\n public class ToastComponent {\n ToastService ToastService { get; set; }\n' +
' void OnInitialized() { ToastService.OnShow += Show; }\n void Show() { }\n }\n}\n'
);
const edge = (await calls('references')).find((e) => e.src === 'OnInitialized' && e.tgt === 'ToastService');
expect(edge?.tgtQn).toBe('App.Helpers::ToastComponent::ToastService');
expect(edge?.confidence).toBe(0.9);
});
it("does not raise a sibling type's member found by its bare name", async () => {
// `Dns()` in one class is not the `Dns` entry of the enum beside it
// (okhttp's InterceptorOverridesTest): no more settled by the file than
// a sibling class's method.
write('src/app/Overrides.kt', 'package app\n\nenum class Param { Dns }\n\nclass Override {\n val value = Dns()\n}\n');
write('src/app/Dns.kt', 'package app\n\nclass Dns\n');
const raised = (await calls()).filter((e) => e.tgtQn === 'app::Param::Dns' && e.confidence >= 0.9);
expect(raised).toEqual([]);
});
});