From 9c173e0aa3e7c1c5ee7e0354a901ad344e7b09f0 Mon Sep 17 00:00:00 2001 From: bompus Date: Thu, 29 Oct 2026 21:46:06 -0600 Subject: [PATCH 1/7] fix(kernel): a JS built-in method name needs receiver evidence (#1987) A call like `list.map()` or `cache.get()` on a receiver the resolver cannot type fell through to name-based method matching, so it linked a same-named project method. The free matching path now declines a built-in method name unless the receiver is rooted at `this`/`super` (a field chain) or at a class name; constructed, imported and typed receivers answer before that path. Fold the js-builtins.ts names into the existing JS_BUILT_IN_METHODS table so the prefilter and the resolver agree on the list. Restores the 11 failing suites on fork/consolidated: full suite 934 files, 11,157 passed, 44 skipped, 0 failed. Golden dumps unchanged; precision holds on vite (6/6 absent, 2/2 present) and svelte (1/1 absent, 0 gained). --- .../precision-svelte-5895c63-4d3ddde3.json | 35 +++++++ .../precision-vite-8492422-4d3ddde3.json | 95 +++++++++++++++++++ codegraph-kernel/src/resolve/method_call.rs | 11 +++ codegraph-kernel/src/resolve/tables.rs | 29 ++++-- 4 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 __tests__/evaluation/results/precision-svelte-5895c63-4d3ddde3.json create mode 100644 __tests__/evaluation/results/precision-vite-8492422-4d3ddde3.json 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-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/codegraph-kernel/src/resolve/method_call.rs b/codegraph-kernel/src/resolve/method_call.rs index 54886f6b1..ddbb51a84 100644 --- a/codegraph-kernel/src/resolve/method_call.rs +++ b/codegraph-kernel/src/resolve/method_call.rs @@ -1076,6 +1076,17 @@ if inferred.is_none() && r.language == "dart" { McShape::Parsed { receiver, method, inferable, dotted } => (receiver, method, inferable, dotted), McShape::Done(res) => return Ok(res), }; + // A built-in method on a bare receiver nothing types is the built-in's: + // the name alone never makes it a project method's (#1987). A receiver + // rooted at `this`/`super` (a field chain) or at a class name is + // evidence of its own; constructed, imported and typed receivers + // resolve before this free path. + if is_js_family(&r.language) && JS_BUILT_IN_METHODS.contains(method_name.as_str()) { + let root = object_or_class.split('.').next().unwrap_or(&object_or_class); + if root != "this" && root != "super" && !root.chars().next().is_some_and(|c| c.is_ascii_uppercase()) { + return Ok(None); + } + } 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)); } diff --git a/codegraph-kernel/src/resolve/tables.rs b/codegraph-kernel/src/resolve/tables.rs index 873d6abde..2ee66b7ed 100644 --- a/codegraph-kernel/src/resolve/tables.rs +++ b/codegraph-kernel/src/resolve/tables.rs @@ -979,20 +979,29 @@ pub(super) static PYTHON_BUILT_INS: LazyLock> = LazyLock:: .collect() }); -/// JS_BUILT_IN_METHODS (import-resolver.ts): methods of the JavaScript -/// built-ins, almost always the built-in when called on an unknown receiver. +/// JS_BUILT_IN_METHODS (import-resolver.ts, js-builtins.ts): methods every +/// JavaScript value inherits (array, string, promise, function, event and +/// iterator methods, and fetch's body methods), almost always the built-in +/// when called on a receiver nothing types. pub(super) static JS_BUILT_IN_METHODS: LazyLock> = LazyLock::new(|| { [ - "push", "pop", "shift", "unshift", "slice", "splice", "concat", "join", "reverse", "sort", "indexOf", - "lastIndexOf", "includes", "find", "findIndex", "findLast", "findLastIndex", "filter", "map", "forEach", - "reduce", "reduceRight", "some", "every", "flat", "flatMap", "fill", "at", "keys", "values", "entries", + "push", "pop", "shift", "unshift", "slice", "splice", "concat", "join", "reverse", "sort", + "indexOf", "lastIndexOf", "includes", "find", "findIndex", "findLast", "findLastIndex", + "filter", "map", "forEach", "reduce", "reduceRight", "some", "every", "flat", "flatMap", + "fill", "at", "keys", "values", "entries", "copyWithin", "toReversed", "toSorted", + "toSpliced", "with", "subarray", "get", "set", "has", "delete", "clear", "add", "then", "catch", "finally", "text", "json", "arrayBuffer", "blob", "formData", - "split", "trim", "trimStart", "trimEnd", "startsWith", "endsWith", "replace", "replaceAll", "match", - "matchAll", "toLowerCase", "toUpperCase", "padStart", "padEnd", "charAt", "charCodeAt", "codePointAt", - "substring", "substr", "repeat", "localeCompare", "normalize", - "toString", "valueOf", "hasOwnProperty", "toJSON", "call", "apply", "bind", - "on", "off", "once", "addEventListener", "removeEventListener", "dispatchEvent", "next", "abort", + "split", "trim", "trimStart", "trimEnd", "trimLeft", "trimRight", "startsWith", "endsWith", + "replace", "replaceAll", "match", "matchAll", "search", "toLowerCase", "toUpperCase", + "toLocaleLowerCase", "toLocaleUpperCase", "padStart", "padEnd", "charAt", "charCodeAt", + "codePointAt", "substring", "substr", "repeat", "localeCompare", "normalize", + "toString", "toLocaleString", "valueOf", "hasOwnProperty", "toJSON", "call", "apply", "bind", + "on", "off", "once", "emit", "addListener", "removeListener", "removeAllListeners", + "prependListener", "prependOnceListener", "listeners", "rawListeners", "listenerCount", + "eventNames", "setMaxListeners", "getMaxListeners", "addEventListener", + "removeEventListener", "dispatchEvent", "next", "return", "throw", "drop", "take", "toArray", + "abort", ] .into_iter() .collect() From 4a59fa59f50818b1ab7ffa027738abf35c57f558 Mon Sep 17 00:00:00 2001 From: bompus Date: Thu, 29 Oct 2026 22:02:28 -0600 Subject: [PATCH 2/7] fix(kernel): keep member evidence around the built-in gate (#1987) The gate sat at the free path's entry, ahead of its field, literal and typed receiver arms, and it widened the prefilter's skip list, which the name matchers consult before any receiver evidence. Both dropped real links found in review of #588: `self.emit()` and `new this().emit()` through the prefilter list, and a class-name receiver that a parameter shadows. Keep the gate list separate from the prefilter's skip list, move the gate below the evidence arms, and exempt a class-name root only while the file does not bind that name to a value. Add a regression test for the bound `self` alias, `new this()`, an unshadowed static call, and the shadowing parameter. Full suite 11,161 passed / 44 skipped / 0 failed; precision holds on vite (6/6 absent, 2/2 present) and svelte (1/1 absent, 0 gained). --- .../precision-svelte-5895c63-9c173e0a.json | 35 +++++++ .../precision-vite-8492422-9c173e0a.json | 95 +++++++++++++++++++ __tests__/js-builtin-method-calls.test.ts | 34 +++++++ codegraph-kernel/src/resolve/method_call.rs | 32 ++++--- codegraph-kernel/src/resolve/tables.rs | 59 ++++++++---- 5 files changed, 224 insertions(+), 31 deletions(-) create mode 100644 __tests__/evaluation/results/precision-svelte-5895c63-9c173e0a.json create mode 100644 __tests__/evaluation/results/precision-vite-8492422-9c173e0a.json 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-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__/js-builtin-method-calls.test.ts b/__tests__/js-builtin-method-calls.test.ts index d850f3fdf..ae38cb60b 100644 --- a/__tests__/js-builtin-method-calls.test.ts +++ b/__tests__/js-builtin-method-calls.test.ts @@ -114,4 +114,38 @@ ${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(); } + later() { const self = this; self.emit(); } + static create() { return new this().emit(); } +} +export class Cache { get() {} } +export function statics() { Bus.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('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::later')).toEqual(['Bus::emit']); + expect(method('Bus::create')).toEqual(['Bus::emit']); + }); }); diff --git a/codegraph-kernel/src/resolve/method_call.rs b/codegraph-kernel/src/resolve/method_call.rs index ddbb51a84..a3a204cfe 100644 --- a/codegraph-kernel/src/resolve/method_call.rs +++ b/codegraph-kernel/src/resolve/method_call.rs @@ -1076,18 +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), }; - // A built-in method on a bare receiver nothing types is the built-in's: - // the name alone never makes it a project method's (#1987). A receiver - // rooted at `this`/`super` (a field chain) or at a class name is - // evidence of its own; constructed, imported and typed receivers - // resolve before this free path. - if is_js_family(&r.language) && JS_BUILT_IN_METHODS.contains(method_name.as_str()) { - let root = object_or_class.split('.').next().unwrap_or(&object_or_class); - if root != "this" && root != "super" && !root.chars().next().is_some_and(|c| c.is_ascii_uppercase()) { - return Ok(None); - } - } - 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)); } } @@ -1310,6 +1298,26 @@ if inferred.is_none() && r.language == "dart" { } + // A built-in method name is not evidence of a project member (#1987): + // `list.map()` and `cache.get()` on a receiver nothing validates are + // the built-in's. The strategies below scan a class or the project by + // the receiver's and the method's names. A receiver rooted at + // `this`/`super` has its own evidence, and a class-name root counts + // only when the file does not bind that name to a value, which would + // shadow the class. Typed, constructed, imported and object-literal + // receivers resolved above. + 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 class_name = root.chars().next().is_some_and(|c| c.is_ascii_uppercase()) + && !self + .receiver_binding(root, r)? + .is_some_and(|b| matches!(b.kind.as_str(), "param" | "local")); + if root != "this" && root != "super" && !class_name { + 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. diff --git a/codegraph-kernel/src/resolve/tables.rs b/codegraph-kernel/src/resolve/tables.rs index 2ee66b7ed..e68e46c17 100644 --- a/codegraph-kernel/src/resolve/tables.rs +++ b/codegraph-kernel/src/resolve/tables.rs @@ -979,29 +979,50 @@ pub(super) static PYTHON_BUILT_INS: LazyLock> = LazyLock:: .collect() }); -/// JS_BUILT_IN_METHODS (import-resolver.ts, js-builtins.ts): methods every -/// JavaScript value inherits (array, string, promise, function, event and -/// iterator methods, and fetch's body methods), almost always the built-in -/// when called on a receiver nothing types. +/// JS_BUILT_IN_METHODS (import-resolver.ts): methods of the JavaScript +/// built-ins, almost always the built-in when called on an unknown receiver. pub(super) static JS_BUILT_IN_METHODS: LazyLock> = LazyLock::new(|| { [ - "push", "pop", "shift", "unshift", "slice", "splice", "concat", "join", "reverse", "sort", - "indexOf", "lastIndexOf", "includes", "find", "findIndex", "findLast", "findLastIndex", - "filter", "map", "forEach", "reduce", "reduceRight", "some", "every", "flat", "flatMap", - "fill", "at", "keys", "values", "entries", "copyWithin", "toReversed", "toSorted", - "toSpliced", "with", "subarray", + "push", "pop", "shift", "unshift", "slice", "splice", "concat", "join", "reverse", "sort", "indexOf", + "lastIndexOf", "includes", "find", "findIndex", "findLast", "findLastIndex", "filter", "map", "forEach", + "reduce", "reduceRight", "some", "every", "flat", "flatMap", "fill", "at", "keys", "values", "entries", "get", "set", "has", "delete", "clear", "add", "then", "catch", "finally", "text", "json", "arrayBuffer", "blob", "formData", - "split", "trim", "trimStart", "trimEnd", "trimLeft", "trimRight", "startsWith", "endsWith", - "replace", "replaceAll", "match", "matchAll", "search", "toLowerCase", "toUpperCase", - "toLocaleLowerCase", "toLocaleUpperCase", "padStart", "padEnd", "charAt", "charCodeAt", - "codePointAt", "substring", "substr", "repeat", "localeCompare", "normalize", - "toString", "toLocaleString", "valueOf", "hasOwnProperty", "toJSON", "call", "apply", "bind", - "on", "off", "once", "emit", "addListener", "removeListener", "removeAllListeners", - "prependListener", "prependOnceListener", "listeners", "rawListeners", "listenerCount", - "eventNames", "setMaxListeners", "getMaxListeners", "addEventListener", - "removeEventListener", "dispatchEvent", "next", "return", "throw", "drop", "take", "toArray", - "abort", + "split", "trim", "trimStart", "trimEnd", "startsWith", "endsWith", "replace", "replaceAll", "match", + "matchAll", "toLowerCase", "toUpperCase", "padStart", "padEnd", "charAt", "charCodeAt", "codePointAt", + "substring", "substr", "repeat", "localeCompare", "normalize", + "toString", "valueOf", "hasOwnProperty", "toJSON", "call", "apply", "bind", + "on", "off", "once", "addEventListener", "removeEventListener", "dispatchEvent", "next", "abort", + ] + .into_iter() + .collect() +}); + +/// JS_BUILTIN_METHODS (js-builtins.ts): method names that require receiver +/// evidence before a call of one may link to project code (#1987). Distinct +/// from JS_BUILT_IN_METHODS, the prefilter's skip list: this one is the +/// resolver's gate, and widening that list changes which same-named member +/// calls the name matchers can still keep. +pub(super) static JS_BUILTIN_METHODS: LazyLock> = LazyLock::new(|| { + [ + "at", "concat", "copyWithin", "entries", "every", "fill", "filter", "find", + "findIndex", "findLast", "findLastIndex", "flat", "flatMap", "forEach", + "includes", "indexOf", "join", "keys", "lastIndexOf", "map", "pop", "push", + "reduce", "reduceRight", "reverse", "shift", "slice", "some", "sort", "splice", + "toReversed", "toSorted", "toSpliced", "unshift", "values", "with", "subarray", + "get", "set", "has", "add", "delete", "clear", + "charAt", "charCodeAt", "codePointAt", "endsWith", "localeCompare", "match", + "matchAll", "normalize", "padEnd", "padStart", "repeat", "replace", "replaceAll", + "search", "split", "startsWith", "substring", "substr", "toLowerCase", + "toUpperCase", "toLocaleLowerCase", "toLocaleUpperCase", "trim", "trimStart", + "trimEnd", "trimLeft", "trimRight", "toString", "toLocaleString", "valueOf", + "then", "catch", "finally", "call", "apply", "bind", + "addEventListener", "removeEventListener", "dispatchEvent", "on", "once", + "off", "emit", "addListener", "removeListener", "removeAllListeners", + "prependListener", "prependOnceListener", "listeners", "rawListeners", + "listenerCount", "eventNames", "setMaxListeners", "getMaxListeners", + "next", "return", "throw", "drop", "take", "toArray", + "text", "json", "arrayBuffer", "blob", "formData", ] .into_iter() .collect() From d6fa6bac5539b12f03af4035efa29ddff321453c Mon Sep 17 00:00:00 2001 From: bompus Date: Thu, 29 Oct 2026 22:04:52 -0600 Subject: [PATCH 3/7] test(kernel): lock the built-in gate's receiver evidence (#1987) Cover the two cases raised in review of #588: a parenthesized `this` receiver keeps a name from the gate list, and a Svelte markup call on a script-typed component receiver resolves before the gate. Both hold on 4a59fa59; the tests keep the table split and the gate's placement from regressing. --- __tests__/js-builtin-method-calls.test.ts | 2 ++ __tests__/js-family-receiver-fixes.test.ts | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/__tests__/js-builtin-method-calls.test.ts b/__tests__/js-builtin-method-calls.test.ts index ae38cb60b..180b41f13 100644 --- a/__tests__/js-builtin-method-calls.test.ts +++ b/__tests__/js-builtin-method-calls.test.ts @@ -122,6 +122,7 @@ export class Bus { emit() {} static get() {} direct() { this.emit(); } + parenthesized() { (this).emit(); } later() { const self = this; self.emit(); } static create() { return new this().emit(); } } @@ -145,6 +146,7 @@ export function unknown(cache) { cache.get(); } 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..4e9dfa9b4 100644 --- a/__tests__/js-family-receiver-fixes.test.ts +++ b/__tests__/js-family-receiver-fixes.test.ts @@ -130,6 +130,27 @@ 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('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 {}; } From b459f9e1f733d8c9b085b97ba535f2cd882ce638 Mon Sep 17 00:00:00 2001 From: bompus Date: Thu, 29 Oct 2026 22:12:30 -0600 Subject: [PATCH 4/7] fix(kernel): a same-line sibling does not shadow a class name (#1987) Binding rows carry line ranges, not columns, so a parameter of another one-line function on the call's own line read as shadowing the class receiver, and the built-in gate declined Bus.get(). Keep a same-line param/local as shadowing only when its name sits inside the caller's own span (its signature, or its body before the call), and cover both sides of the case: the sibling parameter and the caller's own one. --- __tests__/js-builtin-method-calls.test.ts | 2 +- codegraph-kernel/src/resolve/method_call.rs | 49 +++++++++++++++++++-- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/__tests__/js-builtin-method-calls.test.ts b/__tests__/js-builtin-method-calls.test.ts index 180b41f13..62dce0645 100644 --- a/__tests__/js-builtin-method-calls.test.ts +++ b/__tests__/js-builtin-method-calls.test.ts @@ -127,7 +127,7 @@ export class Bus { static create() { return new this().emit(); } } export class Cache { get() {} } -export function statics() { Bus.get(); } +export function unrelated(Bus) {} export function statics() { Bus.get(); } export function shadowed(Cache) { Cache.get(); } export function unknown(cache) { cache.get(); } `); diff --git a/codegraph-kernel/src/resolve/method_call.rs b/codegraph-kernel/src/resolve/method_call.rs index a3a204cfe..b1279e740 100644 --- a/codegraph-kernel/src/resolve/method_call.rs +++ b/codegraph-kernel/src/resolve/method_call.rs @@ -1309,10 +1309,13 @@ if inferred.is_none() && r.language == "dart" { 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 class_name = root.chars().next().is_some_and(|c| c.is_ascii_uppercase()) - && !self - .receiver_binding(root, r)? - .is_some_and(|b| matches!(b.kind.as_str(), "param" | "local")); + let shadowed = match self.receiver_binding(root, r)? { + Some(b) if matches!(b.kind.as_str(), "param" | "local") => { + self.binding_shadows_call(&b, root, r)? + } + _ => false, + }; + let class_name = root.chars().next().is_some_and(|c| c.is_ascii_uppercase()) && !shadowed; if root != "this" && root != "super" && !class_name { return Ok(None); } @@ -1909,6 +1912,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 { + if b.line != r.line { return Ok(true); } + let Some(caller) = self.node_by_id(&r.from_node_id)? else { return Ok(true) }; + if caller.start_line != r.line { return Ok(true); } + let Some(line) = self.read_file(&r.file_path).and_then(|lines| lines.get((r.line - 1) as usize).cloned()) else { + return Ok(true); + }; + let start = caller.start_column.max(0) as usize; + let call = r.column.max(0) as usize; + Ok(line.get(start..call).is_some_and(|span| span_names_identifier(span, root))) + } + /// A `param` binding says nothing when its own position carries no /// annotation — `(root)`, `{ root }: { root: any }` — where `name: T`, /// even `name: any`, does (the claim model pins annotated receivers). @@ -1962,6 +1983,26 @@ if inferred.is_none() && r.language == "dart" { /// parensEnd — UTF-16 unit index one past the ')' that closes the '(' /// at `from - 1`, or -1 when it never closes (JS string indexing). +/// Whether `span` contains `name` as a whole identifier. `$` and `_` are +/// identifier characters in JS. +fn span_names_identifier(span: &str, name: &str) -> bool { + if name.is_empty() { return false; } + let bytes = span.as_bytes(); + let ident = |i: Option| { + i.and_then(|i| bytes.get(i)) + .is_some_and(|c| c.is_ascii_alphanumeric() || matches!(c, b'_' | b'$')) + }; + let mut from = 0; + while let Some(pos) = span[from..].find(name) { + let at = from + pos; + if !ident(at.checked_sub(1)) && !ident(Some(at + name.len())) { + return true; + } + from = at + name.len(); + } + false +} + fn parens_end(s: &str, from: usize) -> i64 { let mut depth = 1i64; let mut units = 0usize; From 20ccaca9816d6c67ea1223a23e8dd8a10fbdc345 Mon Sep 17 00:00:00 2001 From: bompus Date: Thu, 29 Oct 2026 22:13:08 -0600 Subject: [PATCH 5/7] docs(eval): vite and svelte precision at b459f9e1 (#1987) --- .../precision-svelte-5895c63-b459f9e1.json | 35 +++++++ .../precision-vite-8492422-b459f9e1.json | 95 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 __tests__/evaluation/results/precision-svelte-5895c63-b459f9e1.json create mode 100644 __tests__/evaluation/results/precision-vite-8492422-b459f9e1.json 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-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 From f94d87708bf3b0823fe8b5e86e5a18911d634701 Mon Sep 17 00:00:00 2001 From: bompus Date: Thu, 29 Oct 2026 22:17:54 -0600 Subject: [PATCH 6/7] fix(kernel): validate a class receiver before the built-in gate (#1987) The gate required an uppercase first letter as class evidence, so a declared class named `_Cache` or `cache` lost its `get()` link, including a Svelte component's markup where the class scan is the only reader. Run the direct class-name scan before the gate and let the value-binding shadow check stop a parameter or local first; the gate then covers only the name-similarity guesses after that scan. --- __tests__/js-builtin-method-calls.test.ts | 6 ++++ __tests__/js-family-receiver-fixes.test.ts | 22 +++++++++++++ codegraph-kernel/src/resolve/method_call.rs | 34 +++++++++++++-------- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/__tests__/js-builtin-method-calls.test.ts b/__tests__/js-builtin-method-calls.test.ts index 62dce0645..ad4616834 100644 --- a/__tests__/js-builtin-method-calls.test.ts +++ b/__tests__/js-builtin-method-calls.test.ts @@ -127,7 +127,11 @@ export class Bus { 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(); } `); @@ -138,6 +142,8 @@ export function unknown(cache) { cache.get(); } 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) => { diff --git a/__tests__/js-family-receiver-fixes.test.ts b/__tests__/js-family-receiver-fixes.test.ts index 4e9dfa9b4..305a2e5e9 100644 --- a/__tests__/js-family-receiver-fixes.test.ts +++ b/__tests__/js-family-receiver-fixes.test.ts @@ -151,6 +151,28 @@ export function inc() { count.set(1); userCache.set(2); } } }); + 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 b1279e740..2ca88f29f 100644 --- a/codegraph-kernel/src/resolve/method_call.rs +++ b/codegraph-kernel/src/resolve/method_call.rs @@ -1298,27 +1298,21 @@ if inferred.is_none() && r.language == "dart" { } - // A built-in method name is not evidence of a project member (#1987): - // `list.map()` and `cache.get()` on a receiver nothing validates are - // the built-in's. The strategies below scan a class or the project by - // the receiver's and the method's names. A receiver rooted at - // `this`/`super` has its own evidence, and a class-name root counts - // only when the file does not bind that name to a value, which would - // shadow the class. Typed, constructed, imported and object-literal - // receivers resolved above. + // 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 shadowed = match self.receiver_binding(root, r)? { + 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, }; - let class_name = root.chars().next().is_some_and(|c| c.is_ascii_uppercase()) && !shadowed; - if root != "this" && root != "super" && !class_name { - return Ok(None); - } + if value_bound { return Ok(None); } } // A VB.NET receiver the file declares as a variable (`Dim logger As @@ -1334,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); From 6010b64354ba8f344e031a329ea0d6e5127b5860 Mon Sep 17 00:00:00 2001 From: bompus Date: Thu, 29 Oct 2026 22:18:15 -0600 Subject: [PATCH 7/7] docs(eval): vite and svelte precision at f94d8770 (#1987) --- .../precision-svelte-5895c63-f94d8770.json | 35 +++++++ .../precision-vite-8492422-f94d8770.json | 95 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 __tests__/evaluation/results/precision-svelte-5895c63-f94d8770.json create mode 100644 __tests__/evaluation/results/precision-vite-8492422-f94d8770.json 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-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