diff --git a/__tests__/evaluation/results/precision-svelte-5895c63-4d3ddde3.json b/__tests__/evaluation/results/precision-svelte-5895c63-4d3ddde3.json new file mode 100644 index 000000000..86f7f20a8 --- /dev/null +++ b/__tests__/evaluation/results/precision-svelte-5895c63-4d3ddde3.json @@ -0,0 +1,35 @@ +{ + "timestamp": "2026-10-30T03:45:28.721Z", + "corpus": "svelte", + "repo": "https://github.com/sveltejs/svelte.git", + "commit": "5895c63", + "codegraphSha": "4d3ddde3", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 0, + "presentHeld": 0, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 30920, + "import": 22673, + "exact-match": 9038, + "qualified-name": 1832, + "instance-method": 666, + "framework": 220, + "function-ref": 195, + "file-path": 58, + "fuzzy": 13 + }, + "edges": 65615, + "results": [ + { + "caseId": "svelte-bare-import-bundle", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ] +} \ No newline at end of file diff --git a/__tests__/evaluation/results/precision-svelte-5895c63-9c173e0a.json b/__tests__/evaluation/results/precision-svelte-5895c63-9c173e0a.json new file mode 100644 index 000000000..d6da8a2ae --- /dev/null +++ b/__tests__/evaluation/results/precision-svelte-5895c63-9c173e0a.json @@ -0,0 +1,35 @@ +{ + "timestamp": "2026-10-30T04:02:02.833Z", + "corpus": "svelte", + "repo": "https://github.com/sveltejs/svelte.git", + "commit": "5895c63", + "codegraphSha": "9c173e0a", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 0, + "presentHeld": 0, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 30920, + "import": 22673, + "exact-match": 9038, + "qualified-name": 1874, + "instance-method": 683, + "framework": 220, + "function-ref": 195, + "file-path": 58, + "fuzzy": 13 + }, + "edges": 65674, + "results": [ + { + "caseId": "svelte-bare-import-bundle", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ] +} \ No newline at end of file diff --git a/__tests__/evaluation/results/precision-svelte-5895c63-b459f9e1.json b/__tests__/evaluation/results/precision-svelte-5895c63-b459f9e1.json new file mode 100644 index 000000000..ffb3c7ce1 --- /dev/null +++ b/__tests__/evaluation/results/precision-svelte-5895c63-b459f9e1.json @@ -0,0 +1,35 @@ +{ + "timestamp": "2026-10-30T04:12:45.375Z", + "corpus": "svelte", + "repo": "https://github.com/sveltejs/svelte.git", + "commit": "5895c63", + "codegraphSha": "b459f9e1", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 0, + "presentHeld": 0, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 30920, + "import": 22673, + "exact-match": 9038, + "qualified-name": 1874, + "instance-method": 683, + "framework": 220, + "function-ref": 195, + "file-path": 58, + "fuzzy": 13 + }, + "edges": 65674, + "results": [ + { + "caseId": "svelte-bare-import-bundle", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ] +} \ No newline at end of file diff --git a/__tests__/evaluation/results/precision-svelte-5895c63-f94d8770.json b/__tests__/evaluation/results/precision-svelte-5895c63-f94d8770.json new file mode 100644 index 000000000..8ca2351a7 --- /dev/null +++ b/__tests__/evaluation/results/precision-svelte-5895c63-f94d8770.json @@ -0,0 +1,35 @@ +{ + "timestamp": "2026-10-30T04:18:07.908Z", + "corpus": "svelte", + "repo": "https://github.com/sveltejs/svelte.git", + "commit": "5895c63", + "codegraphSha": "f94d8770", + "summary": { + "absentCases": 1, + "absentHeld": 1, + "presentCases": 0, + "presentHeld": 0, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 30920, + "import": 22673, + "exact-match": 9038, + "qualified-name": 1874, + "instance-method": 683, + "framework": 220, + "function-ref": 195, + "file-path": 58, + "fuzzy": 13 + }, + "edges": 65674, + "results": [ + { + "caseId": "svelte-bare-import-bundle", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + } + ] +} \ No newline at end of file diff --git a/__tests__/evaluation/results/precision-vite-8492422-4d3ddde3.json b/__tests__/evaluation/results/precision-vite-8492422-4d3ddde3.json new file mode 100644 index 000000000..2efe326df --- /dev/null +++ b/__tests__/evaluation/results/precision-vite-8492422-4d3ddde3.json @@ -0,0 +1,95 @@ +{ + "timestamp": "2026-10-30T03:45:22.193Z", + "corpus": "vite", + "repo": "https://github.com/vitejs/vite.git", + "commit": "8492422", + "codegraphSha": "4d3ddde3", + "summary": { + "absentCases": 6, + "absentHeld": 6, + "presentCases": 2, + "presentHeld": 2, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 13392, + "import": 11144, + "exact-match": 3386, + "qualified-name": 1009, + "instance-method": 536, + "file-path": 197, + "function-ref": 163, + "framework": 1 + }, + "edges": 29828, + "results": [ + { + "caseId": "vite-factory-receiver-control", + "expect": "present", + "pass": true, + "found": [ + { + "from": "function:d321dd128d90578bc69867835ec73ef8", + "to": "method:db73e5f5d9c7af61d0f22c5b9971ced3", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "vite-external-receiver-not-project-method", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-self-edge", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-resolveConfig-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-build-scan", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-sealed-module-defineConfig", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-control-relative-import", + "expect": "present", + "pass": true, + "found": [ + { + "from": "file:packages/vite/src/node/config.ts", + "to": "function:0c2f36c32c5742aee412978220fd5b23", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + } + ] +} \ No newline at end of file diff --git a/__tests__/evaluation/results/precision-vite-8492422-9c173e0a.json b/__tests__/evaluation/results/precision-vite-8492422-9c173e0a.json new file mode 100644 index 000000000..60e672a56 --- /dev/null +++ b/__tests__/evaluation/results/precision-vite-8492422-9c173e0a.json @@ -0,0 +1,95 @@ +{ + "timestamp": "2026-10-30T04:02:01.114Z", + "corpus": "vite", + "repo": "https://github.com/vitejs/vite.git", + "commit": "8492422", + "codegraphSha": "9c173e0a", + "summary": { + "absentCases": 6, + "absentHeld": 6, + "presentCases": 2, + "presentHeld": 2, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 13392, + "import": 11144, + "exact-match": 3386, + "qualified-name": 1009, + "instance-method": 536, + "file-path": 197, + "function-ref": 163, + "framework": 1 + }, + "edges": 29828, + "results": [ + { + "caseId": "vite-factory-receiver-control", + "expect": "present", + "pass": true, + "found": [ + { + "from": "function:d321dd128d90578bc69867835ec73ef8", + "to": "method:db73e5f5d9c7af61d0f22c5b9971ced3", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "vite-external-receiver-not-project-method", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-self-edge", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-resolveConfig-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-build-scan", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-sealed-module-defineConfig", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-control-relative-import", + "expect": "present", + "pass": true, + "found": [ + { + "from": "file:packages/vite/src/node/config.ts", + "to": "function:0c2f36c32c5742aee412978220fd5b23", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + } + ] +} \ No newline at end of file diff --git a/__tests__/evaluation/results/precision-vite-8492422-b459f9e1.json b/__tests__/evaluation/results/precision-vite-8492422-b459f9e1.json new file mode 100644 index 000000000..64ab1efe3 --- /dev/null +++ b/__tests__/evaluation/results/precision-vite-8492422-b459f9e1.json @@ -0,0 +1,95 @@ +{ + "timestamp": "2026-10-30T04:12:43.668Z", + "corpus": "vite", + "repo": "https://github.com/vitejs/vite.git", + "commit": "8492422", + "codegraphSha": "b459f9e1", + "summary": { + "absentCases": 6, + "absentHeld": 6, + "presentCases": 2, + "presentHeld": 2, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 13392, + "import": 11144, + "exact-match": 3386, + "qualified-name": 1009, + "instance-method": 536, + "file-path": 197, + "function-ref": 163, + "framework": 1 + }, + "edges": 29828, + "results": [ + { + "caseId": "vite-factory-receiver-control", + "expect": "present", + "pass": true, + "found": [ + { + "from": "function:d321dd128d90578bc69867835ec73ef8", + "to": "method:db73e5f5d9c7af61d0f22c5b9971ced3", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "vite-external-receiver-not-project-method", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-self-edge", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-resolveConfig-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-build-scan", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-sealed-module-defineConfig", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-control-relative-import", + "expect": "present", + "pass": true, + "found": [ + { + "from": "file:packages/vite/src/node/config.ts", + "to": "function:0c2f36c32c5742aee412978220fd5b23", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + } + ] +} \ No newline at end of file diff --git a/__tests__/evaluation/results/precision-vite-8492422-f94d8770.json b/__tests__/evaluation/results/precision-vite-8492422-f94d8770.json new file mode 100644 index 000000000..c9ddc358c --- /dev/null +++ b/__tests__/evaluation/results/precision-vite-8492422-f94d8770.json @@ -0,0 +1,95 @@ +{ + "timestamp": "2026-10-30T04:18:06.156Z", + "corpus": "vite", + "repo": "https://github.com/vitejs/vite.git", + "commit": "8492422", + "codegraphSha": "f94d8770", + "summary": { + "absentCases": 6, + "absentHeld": 6, + "presentCases": 2, + "presentHeld": 2, + "unjudgeable": 0 + }, + "resolvedBy": { + "(none)": 13392, + "import": 11144, + "exact-match": 3386, + "qualified-name": 1009, + "instance-method": 536, + "file-path": 197, + "function-ref": 163, + "framework": 1 + }, + "edges": 29828, + "results": [ + { + "caseId": "vite-factory-receiver-control", + "expect": "present", + "pass": true, + "found": [ + { + "from": "function:d321dd128d90578bc69867835ec73ef8", + "to": "method:db73e5f5d9c7af61d0f22c5b9971ced3", + "resolvedBy": "instance-method" + } + ], + "missingEndpoints": [] + }, + { + "caseId": "vite-external-receiver-not-project-method", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-self-edge", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-bare-import-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-resolveConfig-getEnv", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-fuzzy-nested-build-scan", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-sealed-module-defineConfig", + "expect": "absent", + "pass": true, + "found": [], + "missingEndpoints": [] + }, + { + "caseId": "vite-control-relative-import", + "expect": "present", + "pass": true, + "found": [ + { + "from": "file:packages/vite/src/node/config.ts", + "to": "function:0c2f36c32c5742aee412978220fd5b23", + "resolvedBy": "import" + } + ], + "missingEndpoints": [] + } + ] +} \ No newline at end of file diff --git a/__tests__/js-builtin-method-calls.test.ts b/__tests__/js-builtin-method-calls.test.ts index d850f3fdf..ad4616834 100644 --- a/__tests__/js-builtin-method-calls.test.ts +++ b/__tests__/js-builtin-method-calls.test.ts @@ -114,4 +114,46 @@ ${typed ? 'export function typed(cart: Cart) { cart.map(); }' : ''} const self = cg.getNodesByKind('method').find(n => n.qualifiedName === 'Cart::self')!; expect(cg.getCallees(self.id).map(c => c.node.qualifiedName)).toEqual(['Cart::add']); }); + + it('keeps the evidence a receiver spelling can carry, and rejects a class name the file shadows', async () => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-1987-')); + fs.writeFileSync(path.join(dir, `members.${ext}`), ` +export class Bus { + emit() {} + static get() {} + direct() { this.emit(); } + parenthesized() { (this).emit(); } + later() { const self = this; self.emit(); } + static create() { return new this().emit(); } +} +export class Cache { get() {} } +export class _Cache { static get() {} } +export class cache { static get() {} } +export function unrelated(Bus) {} export function statics() { Bus.get(); } +export function underscore() { _Cache.get(); } +export function lowercase() { cache.get(); } +export function shadowed(Cache) { Cache.get(); } +export function unknown(cache) { cache.get(); } +`); + cg = await CodeGraph.init(dir, { index: true }); + const callees = (name: string) => { + const fn = cg!.getNodesByKind('function').find(n => n.name === name)!; + expect(fn, name).toBeDefined(); + return cg!.getCallees(fn.id).filter(({ edge }) => edge.kind === 'calls').map(c => c.node.qualifiedName).sort(); + }; + expect(callees('statics')).toEqual(['Bus::get']); + expect(callees('underscore')).toEqual(['_Cache::get']); + expect(callees('lowercase')).toEqual(['cache::get']); + expect(callees('shadowed')).toEqual([]); + expect(callees('unknown')).toEqual([]); + const method = (qn: string) => { + const n = cg!.getNodesByKind('method').find(n => n.qualifiedName === qn)!; + expect(n, qn).toBeDefined(); + return cg!.getCallees(n.id).filter(({ edge }) => edge.kind === 'calls').map(c => c.node.qualifiedName); + }; + expect(method('Bus::direct')).toEqual(['Bus::emit']); + expect(method('Bus::parenthesized')).toEqual(['Bus::emit']); + expect(method('Bus::later')).toEqual(['Bus::emit']); + expect(method('Bus::create')).toEqual(['Bus::emit']); + }); }); diff --git a/__tests__/js-family-receiver-fixes.test.ts b/__tests__/js-family-receiver-fixes.test.ts index 87b7220fc..305a2e5e9 100644 --- a/__tests__/js-family-receiver-fixes.test.ts +++ b/__tests__/js-family-receiver-fixes.test.ts @@ -130,6 +130,49 @@ export function inc() { count.set(1); userCache.set(2); } } }); + it('keeps an inferred markup receiver for a built-in method name (#1987)', async () => { + // The component's script types `counter`; the markup call must reach the + // type before the built-in gate considers the receiver's spelling. + const cg = await index({ + 'src/Store.svelte': ` + + +`, + }); + try { + expect(callsFrom(cg, 'src/Store.svelte')).toEqual(['src/Store.svelte:Store::get@0.9']); + } finally { + cg.close(); + } + }); + + it('keeps a class receiver named with an underscore or lowercase in markup (#1987)', async () => { + // The class-name scan validates the receiver, whatever its spelling; the + // built-in gate must not turn `_Cache.get()` or `cache.get()` into guesses. + const cg = await index({ + 'src/Cache.svelte': ` + +
{_Cache.get()}{cache.get()}
+`, + }); + try { + expect(callsFrom(cg, 'src/Cache.svelte')).toEqual([ + 'src/Cache.svelte:_Cache::get@0.85', + 'src/Cache.svelte:cache::get@0.85', + ]); + } finally { + cg.close(); + } + }); + it('takes a script-level import as the receiver in Svelte markup and a second script', async () => { const cg = await index({ 'src/spread.js': `export function spread() { return {}; } diff --git a/codegraph-kernel/src/resolve/method_call.rs b/codegraph-kernel/src/resolve/method_call.rs index 54886f6b1..2ca88f29f 100644 --- a/codegraph-kernel/src/resolve/method_call.rs +++ b/codegraph-kernel/src/resolve/method_call.rs @@ -1076,7 +1076,6 @@ if inferred.is_none() && r.language == "dart" { McShape::Parsed { receiver, method, inferable, dotted } => (receiver, method, inferable, dotted), McShape::Done(res) => return Ok(res), }; - if matches!(r.language.as_str(), "java" | "kotlin") { if let Some(hit) = self.enum_constant_call(&object_or_class, &method_name, r)? { return Ok(Some(hit)); } } @@ -1299,6 +1298,23 @@ if inferred.is_none() && r.language == "dart" { } + // A built-in method called on a value the file binds — a parameter or + // local, which shadows any class of that name — is the built-in's + // (#1987): `list.map()`, `cache.get()`. Constructed, imported, typed + // and object-literal receivers resolved above; a class receiver is + // validated by the strategy below before the name-only scan. + if r.reference_kind == "calls" && is_js_family(&r.language) + && JS_BUILTIN_METHODS.contains(method_name.as_str()) { + let root = object_or_class.split('.').next().unwrap_or(&object_or_class); + let value_bound = match self.receiver_binding(root, r)? { + Some(b) if matches!(b.kind.as_str(), "param" | "local") => { + self.binding_shadows_call(&b, root, r)? + } + _ => false, + }; + if value_bound { return Ok(None); } + } + // A VB.NET receiver the file declares as a variable (`Dim logger As // New FileLogger()`) is a value; that its spelling matches a type // (`Logger`, names being case-insensitive) says nothing of its type. @@ -1312,6 +1328,20 @@ if inferred.is_none() && r.language == "dart" { } } + // A built-in method name is not evidence of a project member (#1987). + // The direct class-name strategy above validates a class, whatever its + // spelling; the name-similarity strategies below — a capitalized + // receiver, a `$`-suffixed receiver, a unique method name — are + // guesses, so a built-in name stops here, except on `this`/`super`, + // whose member belongs to the enclosing class. + if r.reference_kind == "calls" && is_js_family(&r.language) + && JS_BUILTIN_METHODS.contains(method_name.as_str()) { + let root = object_or_class.split('.').next().unwrap_or(&object_or_class); + if root != "this" && root != "super" { + return Ok(None); + } + } + if r.language == "csharp" { if let Some(alias) = self.csharp_alias_at(&object_or_class, r) { let name = alias.rsplit('.').next().unwrap_or(&alias); @@ -1890,6 +1920,24 @@ if inferred.is_none() && r.language == "dart" { Ok(re!(r"^[!?]?\s*:").is_match(&line[m.end()..])) } + /// Whether a `param`/`local` row whose line scope contains the call was + /// declared in the call's own function. Binding rows carry lines, not + /// columns, so a one-line function and a sibling on the same line share + /// every line; a name inside the caller's own span (its signature, or its + /// body before the call) is the caller's, while a name before that span + /// belongs to the sibling and does not shadow a class of the same name. + fn binding_shadows_call(&mut self, b: &KBinding, root: &str, r: &ResolveRefIn) -> Res