From 2e29a034676ee8bc2b060983af24829fb279c26d Mon Sep 17 00:00:00 2001 From: ReidenXerx Date: Thu, 6 Aug 2026 20:18:08 +0300 Subject: [PATCH] feat(scope-resolution): capture destructured parameter keys as property reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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) --- .../ingestion/languages/javascript/query.ts | 38 +++++++++++++++++++ .../destructured.js | 19 ++++++++++ .../javascript-object-properties.test.ts | 20 ++++++++++ 3 files changed, 77 insertions(+) create mode 100644 gitnexus/test/fixtures/lang-resolution/javascript-object-properties/destructured.js diff --git a/gitnexus/src/core/ingestion/languages/javascript/query.ts b/gitnexus/src/core/ingestion/languages/javascript/query.ts index b9949c065..5b0b5cb1a 100644 --- a/gitnexus/src/core/ingestion/languages/javascript/query.ts +++ b/gitnexus/src/core/ingestion/languages/javascript/query.ts @@ -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. */ diff --git a/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/destructured.js b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/destructured.js new file mode 100644 index 000000000..17e3c2f7c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/javascript-object-properties/destructured.js @@ -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; +} diff --git a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts index 367b58549..629e2f44a 100644 --- a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts +++ b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts @@ -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