From 37509214868bbc8cfa7671455f9f5a46901e30e9 Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Fri, 23 Oct 2026 16:03:32 -0500 Subject: [PATCH] test(resolution): calls on nested receivers keep their evidence and guess no target A member call on a nested receiver, like holder?.values.get(readKey()), has failed both ways. Before PR #1790, each such call was linked by its bare method name, to any project method called get. PR #1790 stopped that by dropping every call on a chain rooted at a name, which also dropped the call sites effects are drawn from (issue #1794). PR #1862 brought the dotted chains back as unresolved references; shapes its chain pattern rejects, like holder[0].get(), are still dropped. The test indexes eight receiver shapes in .ts, .tsx, .js and .jsx: optional access, computed and numeric keys, a quoted key, a comment inside the receiver, a non-ASCII name, an imported root and a namespace import. It checks that the readKey() argument call survives, that nothing links to Collision.get or to the imported object, and that the three dotted chains (holder.values.get, imported.values.get and data.holder.values.get) are still in the index, unresolved. That last check is ours. As first written, the test passed on PR 1790's commit de5adba7, so it could not see the drop issue 1794 is about. With it, the test fails 4 of 4 on de5adba7 (the chains are gone) and 4 of 4 on its parent 72c1ff13 (every shape linked Collision::get), with the kernel and with CODEGRAPH_KERNEL=0, and passes 4 of 4 on main d02a88d0 with both. On main's code, putting the drop back fails it, as it fails release-main-regressions' retention test; letting the shapes the chain pattern rejects fall back to the bare method name fails only this test. The non-ASCII name is written with U+00F6 and U+00E9, Latin-1 letters, not homoglyphs. Test-only, so no CHANGELOG entry. Original PR #1844 by @bompus. Co-Authored-By: Claude Opus 5.5 --- __tests__/resolution.test.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index 14b5198f4..738f13af9 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -2453,6 +2453,37 @@ func main() { }); describe('Local-variable receiver-type inference (#1108)', () => { + it.each(['ts', 'tsx', 'js', 'jsx'])('keeps compound receiver evidence without guessing targets — %s (#1794)', async (ext) => { + fs.writeFileSync(path.join(tempDir, `holder.${ext}`), 'export const holder = { values: new Map() };'); + const receivers = [ + 'holder?.values', 'holder[readKey()]', 'holder[0]', 'holder["odd.key"]', + 'holder /* receiver */.values', 'höldér.values', 'imported.values', 'data.holder.values', + ]; + fs.writeFileSync(path.join(tempDir, `calls.${ext}`), ` +import { holder as imported } from './holder'; +import * as data from './holder'; +export class Collision { get(key) { return key; } } +export function readKey() { return 'values'; } +${receivers.map((receiver, i) => `export function nested${i}(holder, höldér) { return ${receiver}.get(readKey()); }`).join('\n')} +`); + // A dotted chain stays in the index, unresolved, as the call's evidence + // (#1790 dropped these calls outright). + const kept: Record = { 0: 'holder.values.get', 6: 'imported.values.get', 7: 'data.holder.values.get' }; + cg = await CodeGraph.init(tempDir, { index: true }); + for (let i = 0; i < receivers.length; i++) { + const caller = cg.getNodesByName(`nested${i}`).find(n => n.kind === 'function')!; + const callees = cg.getCallees(caller.id).filter(({ edge }) => edge.kind === 'calls'); + // Argument and computed-key calls survive, but neither an imported + // root object nor Collision.get is evidence of the called member. + expect(callees.length, receivers[i]).toBeGreaterThan(0); + expect(callees.every(({ node }) => node.name === 'readKey'), receivers[i]).toBe(true); + if (kept[i]) { + const refs = cg.getUnresolvedReferencesFrom(caller.id).filter((r) => r.referenceKind === 'calls'); + expect(refs.map((r) => r.referenceName), receivers[i]).toEqual([kept[i]]); + } + } + }); + it.each(['ts', 'tsx', 'js', 'jsx'])('keeps built-in Map calls off project methods — %s (#1566)', async (ext) => { const typed = ext === 'ts' || ext === 'tsx'; fs.writeFileSync(path.join(tempDir, `cache.${ext}`), `