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