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 <noreply@anthropic.com>
This commit is contained in:
Aaron Queen
2026-10-23 21:06:53 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent d02a88d052
commit 3750921486
+31
View File
@@ -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<number, string> = { 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}`), `