feat(scope-resolution): capture destructured parameter keys as property reads

`function exit({ exitMinAtrMult = 0 })` reads that property off whatever the
caller passes, exactly as `cfg.exitMinAtrMult` would. It never appears in a
member_expression, so it had no reference site at all — and this is the shape
the function that IMPLEMENTS a behaviour uses, so the most relevant reader was
the one systematically missing from "who reads this setting?".

Uses a distinct `@reference.read.destructured` anchor rather than
`@reference.read.member`. The latter is filtered emit-side to matches with a
member_expression ancestor, because calls and writes share its shape, and a
destructuring pattern has none — reusing the tag would have been silently
dropped by that filter. The `read.` head already maps to a read kind, so no
mapping change is needed.

Scoped to formal_parameters. A destructuring binding elsewhere
(`const { x } = require('m')`) is frequently an import rather than a field
read, and minting a property read there would attribute module bindings to
unrelated same-named keys.

All three cases (default value, bare shorthand, renamed key) mutation-checked
by removing the patterns and confirming those three tests, and only those,
fail. The renamed case also asserts the edge points at the KEY and that the
local alias mints nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ReidenXerx 2026-08-06 20:18:08 +03:00
parent 98f9f5405f
commit 2e29a03467
3 changed files with 77 additions and 0 deletions

View file

@ -609,6 +609,44 @@ export const JAVASCRIPT_SCOPE_QUERY = `
(return_statement
(identifier) @reference.name @reference.read.identifier)
;; Destructured PARAMETER keys (R2-1c). \`function exit({ exitMinAtrMult = 0 })\`
;; reads that property off whatever the caller passes, exactly as
;; \`cfg.exitMinAtrMult\` would — the field just never appears in a
;; member_expression, so the read had no site at all and the function that
;; implements the behaviour was missing from "who reads this setting?".
;;
;; A distinct anchor rather than @reference.read.member: that tag is filtered
;; emit-side to matches with a member_expression ancestor (calls and writes
;; share its shape), and a destructuring pattern has none, so it would be
;; dropped. The \`read.\` head is what maps this to a read kind, so the new tag
;; needs no mapping change.
;;
;; The object_pattern is the receiver. It is anonymous — there is no name to
;; type — which is precisely the untyped-receiver case the name-narrowing pass
;; exists to serve.
;;
;; Scoped to formal_parameters deliberately. A destructuring binding elsewhere
;; (\`const { x } = require('m')\`) is often an import rather than a field read,
;; and minting a property read for it would attribute module bindings to
;; unrelated same-named keys.
(formal_parameters
(object_pattern
(shorthand_property_identifier_pattern) @reference.name
@reference.read.destructured) @reference.receiver)
(formal_parameters
(object_pattern
(object_assignment_pattern
left: (shorthand_property_identifier_pattern) @reference.name
@reference.read.destructured)) @reference.receiver)
(formal_parameters
(object_pattern
(pair_pattern
key: (property_identifier) @reference.name
@reference.read.destructured)) @reference.receiver)
`;
/** JSX-only suffix — appended when compiling against the JSX grammar for .jsx files. */

View file

@ -0,0 +1,19 @@
// R2-1c: the function that IMPLEMENTS the behaviour reads its settings by
// destructuring them out of the argument. The field never appears in a
// member_expression, so before this it had no read site at all and the most
// relevant reader was missing from "who reads this setting?".
export const destructuredDefaults = {
destructuredOnlyField: 7,
};
export function appliesDestructured({ destructuredOnlyField = 0 }) {
return destructuredOnlyField * 2;
}
export function appliesRenamed({ destructuredOnlyField: aliased }) {
return aliased;
}
export function appliesShorthand({ destructuredOnlyField }) {
return destructuredOnlyField;
}

View file

@ -104,6 +104,26 @@ describe('JavaScript plain-object property access (A1/A5)', () => {
for (const e of inferred) expect(e.rel.confidence).toBeLessThan(0.85);
});
// R2-1c. The function that implements a behaviour usually destructures its
// settings out of the argument rather than reaching through a receiver, so
// the most relevant reader was the one shape with no read site at all.
describe('destructured parameters (R2-1c)', () => {
it('emits ACCESSES for a destructured key with a default', () => {
expect(readersOf('destructuredOnlyField')).toContain('appliesDestructured');
});
it('emits ACCESSES for bare shorthand destructuring', () => {
expect(readersOf('destructuredOnlyField')).toContain('appliesShorthand');
});
// `{ field: alias }` reads `field` and binds `alias`; the READ is of the
// key, so the edge must point at the key rather than the local name.
it('follows the key, not the local alias, when renamed', () => {
expect(readersOf('destructuredOnlyField')).toContain('appliesRenamed');
expect(propertyNames()).not.toContain('aliased');
});
});
// R2. Strict workspace uniqueness was measurably too blunt: in the reporting
// repo `exitMinAtrMult` had 26 definitions, 16 of them in one-off scripts the
// backend has no relationship with, so every backend read was refused because