feat(javascript): index object-literal keys of a named object as Property nodes

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) <noreply@anthropic.com>
This commit is contained in:
ReidenXerx 2026-08-06 00:07:48 +03:00
parent 7eabdd46d6
commit 3d9ed40276
3 changed files with 81 additions and 19 deletions

View file

@ -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

View file

@ -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

View file

@ -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<PropNode> } }).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<string, unknown>;
}