From 3d9ed40276a32ab70dc10a7a1faf8a5b4e2f2466 Mon Sep 17 00:00:00 2001 From: ReidenXerx Date: Thu, 6 Aug 2026 00:07:48 +0300 Subject: [PATCH] feat(javascript): index object-literal keys of a named object as Property nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Idiomatic JS models configuration as an object literal, not a class, but Property definition nodes existed only for DECLARED CLASS FIELDS. A config field therefore had no symbol at all: `context({name: 'exitMinAtrMult'})` answered "not found" for a field read and written throughout a live code path, and ACCESSES had no target to point at. Both halves are added for keys of a literal BOUND TO A VARIABLE — the parse query mints the graph node, the scope query mints the def the resolver can aim at. Unbound literals are deliberately excluded: an inline call argument or a JSX prop bag is call-site data, not a named surface other code references, so a node per key there would add volume without adding an answerable question. This lands the definition-node half only. The ACCESSES edges still require receiver resolution — typing the const that holds the literal to the literal's scope for the precise case, and name-based matching at reduced confidence for the untyped-param (option bag) case. Both are recorded as todos with the mechanism each needs. Also records a trap that cost a wrong conclusion: under vitest the parse worker runs the BUILT dist code (parse-impl resolves parse-worker.js, absent under src/, and falls back to dist), so parse-query changes are invisible to tests until `npm run build`. Co-Authored-By: Claude Opus 5 (1M context) --- .../ingestion/languages/javascript/query.ts | 9 +++ .../src/core/ingestion/tree-sitter-queries.ts | 14 ++++ .../javascript-object-properties.test.ts | 77 ++++++++++++++----- 3 files changed, 81 insertions(+), 19 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/javascript/query.ts b/gitnexus/src/core/ingestion/languages/javascript/query.ts index d7fa30fc3..b9949c065 100644 --- a/gitnexus/src/core/ingestion/languages/javascript/query.ts +++ b/gitnexus/src/core/ingestion/languages/javascript/query.ts @@ -107,6 +107,15 @@ export const JAVASCRIPT_SCOPE_QUERY = ` (field_definition property: (property_identifier) @declaration.name) @declaration.property +;; Object-literal keys of a NAMED object (A1/A5) — the scope-resolution half of +;; the same rule in TYPESCRIPT/JAVASCRIPT_QUERIES. The parse query mints the +;; Property NODE; this mints the DEF the resolver can point a read/write at. +(variable_declarator + name: (identifier) + value: (object + (pair + key: (property_identifier) @declaration.name) @declaration.property)) + ;; Declarations — free functions (function_declaration name: (identifier) @declaration.name) @declaration.function diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 765e8492b..54a22d000 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -848,6 +848,20 @@ export const JAVASCRIPT_QUERIES = ` (field_definition property: (property_identifier) @name) @definition.property +; Object-literal keys of a NAMED object (A1/A5). Idiomatic JS models config as +; an object literal, not a class, so without these the fields of an options bag +; have no node and "who reads/writes this setting?" answers a confident zero. +; +; Deliberately scoped to a literal BOUND TO A VARIABLE. An unbound literal is +; usually an inline call argument or a JSX prop bag, whose keys are call-site +; data rather than a named surface other code references — minting a node per +; key there would add volume without adding an answerable question. +(variable_declarator + name: (identifier) + value: (object + (pair + key: (property_identifier) @name) @definition.property)) + ; Closure-valued class fields (#2693) — see the TypeScript block for why these ; are Method rather than Property. (field_definition diff --git a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts index 2bea184d1..c431d4875 100644 --- a/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts +++ b/gitnexus/test/integration/resolvers/javascript-object-properties.test.ts @@ -15,27 +15,35 @@ * that dominates idiomatic JS. Not precisely solvable without types; * covered by name-based fallback at reduced confidence. * - * STATUS — not yet implemented; these are the acceptance criteria. + * STATUS — definition nodes DONE, edge resolution REMAINING. * - * Established so far: - * - A parse-query pattern scoped to object literals BOUND TO A VARIABLE - * (`(variable_declarator name: (identifier) value: (object (pair key: - * (property_identifier) @name) @definition.property))`) matches correctly: - * verified against the raw JAVASCRIPT_QUERIES, 4 captures on this fixture - * with the right names. It is NOT enough on its own — no `Property` node - * reaches the graph, and `local-symbol-pruner` is not the cause (it drops - * only Const/Variable/Static). The remaining gate is in the parse worker's - * node-creation path for `@definition.property` captures. - * - The two receiver shapes need different mechanisms. Through the holding - * variable the receiver is typeable and must resolve precisely. Through an - * untyped param it is not, and needs name-based matching — sanctioned for - * dynamic languages here (`fieldFallbackOnMethodLookup` defaults on, and - * the Vue provider documents it recovering plain-object-literal cases) but - * it must carry reduced confidence so precision is not overclaimed. + * Done: object-literal keys bound to a variable now mint both halves — the + * graph `Property` node (JAVASCRIPT_QUERIES) and the scope-resolution def + * (languages/javascript/query.ts). A config field is findable by name where it + * previously did not exist as a symbol at all. + * + * Remaining: the ACCESSES edges. The two receiver shapes need different + * mechanisms and neither is implemented: + * - Through the holding variable (`exitRules.exitMinAtrMult`) the receiver is + * typeable, so it must resolve precisely — the Const holding the literal + * has to be typed to the literal's scope, the way `classScopeByDefId` maps + * a class def to its scope. + * - Through an untyped param (`cfg.exitMinAtrMult`) it is not typeable in + * plain JS and needs name-based matching — sanctioned for dynamic languages + * here (`fieldFallbackOnMethodLookup` defaults on, and the Vue provider + * documents it recovering plain-object-literal cases) but it must carry + * reduced confidence so precision is not overclaimed. + * + * TRAP, learned the hard way and recorded so the next reader does not repeat + * it: under vitest the PARSE WORKER runs the BUILT `dist/` code, because + * `parse-impl.ts` resolves `../workers/parse-worker.js`, which does not exist + * under `src/`, and falls back to dist. Scope resolution runs from `src`. So a + * change to TYPESCRIPT/JAVASCRIPT_QUERIES is invisible to tests until + * `npm run build` — it reads exactly like a failed hypothesis. */ -import { describe, it, beforeAll } from 'vitest'; +import { describe, it, expect, beforeAll } from 'vitest'; import path from 'path'; -import { FIXTURES, runPipelineFromRepo, type PipelineResult } from './helpers.js'; +import { FIXTURES, getRelationships, runPipelineFromRepo, type PipelineResult } from './helpers.js'; describe('JavaScript plain-object property access (A1/A5)', () => { let result: PipelineResult; @@ -47,8 +55,39 @@ describe('JavaScript plain-object property access (A1/A5)', () => { ); }, 60000); - it.todo('indexes object-literal keys as Property nodes'); + const propertyNames = (): string[] => + Array.from( + (result as unknown as { graph: { iterNodes(): Iterable } }).graph.iterNodes(), + ) + .filter((n) => n.label === 'Property') + .map((n) => String(n.properties.name)); + + const readersOf = (field: string): string[] => + getRelationships(result, 'ACCESSES') + .filter((e) => e.target === field) + .map((e) => e.source); + + it('indexes object-literal keys as Property nodes', () => { + const props = propertyNames(); + expect(props).toContain('exitMinAtrMult'); + expect(props).toContain('stopAtrMult'); + }); + + it('gives every indexed key a distinct node, not one merged symbol', () => { + const props = propertyNames().filter((n) => n === 'exitMinAtrMult' || n === 'stopAtrMult'); + expect(new Set(props).size).toBe(2); + }); + + // Blocked on receiver resolution — see STATUS above. `readersOf` is the + // assertion these become once the receiver can be typed to the literal. it.todo('emits ACCESSES for a read through the holding variable'); it.todo('emits ACCESSES for the property WRITE (A5)'); it.todo('emits ACCESSES for a read through an untyped param (option bag)'); + + void readersOf; }); + +interface PropNode { + readonly label: string; + readonly properties: Record; +}