mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat(typescript): index object-literal keys, as JavaScript already did
The sibling recorded in `0c5a4f64` and deliberately left out of it. Both the
named object-literal rule (A1/A5) and the identity-wrapper rule (R2-1a) lived
only in JAVASCRIPT_QUERIES, so the single most common config idiom in
TypeScript —
export const tsRuntimeConfig = { tsConfigRetries: 3 };
— minted no node for any key. `context()` answered "Symbol not found" and a
precise read through the holding variable had nothing to resolve to.
TypeScript sets `fieldFallbackOnMethodLookup: false`, so these gain no
name-based inference. What they gain is the PRECISE path, which is the route
TypeScript is meant to use: `tsRuntimeConfig.tsConfigRetries` has a typeable
receiver and now resolves. A read through an untyped receiver stays unresolved
and, since `0c5a4f64`, is reported as such rather than answering an empty set.
Scoped exactly as the JavaScript rules are — bound to a variable, and for the
wrapper only the three functions that return the argument they were given —
with the same `Object.entries` negative control pinning the allowlist.
Found by fixture, not by report: the first draft of the `reportOnly` test used
a TS `const CONFIG = { ... }` as its discriminator and could not discriminate,
because the shape mints nothing. That is the whole argument for sweeping a
class instead of waiting for each instance to be filed.
SCHEMA_BUMP 47 -> 48: parse-time, so a warm cache replays ParsedFiles carrying
none of these matches and the keys stay invisible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0c5a4f64bf
commit
8972d223a9
6 changed files with 128 additions and 3 deletions
|
|
@ -517,6 +517,28 @@ export const TYPESCRIPT_SCOPE_QUERY = `
|
|||
(property_signature
|
||||
name: (property_identifier) @declaration.name) @declaration.property)
|
||||
|
||||
;; Object-literal keys of a NAMED object — the scope-resolution half of the
|
||||
;; matching rule in TYPESCRIPT_QUERIES. The parse query mints the Property NODE;
|
||||
;; this mints the DEF a precise read can resolve to.
|
||||
(variable_declarator
|
||||
name: (identifier)
|
||||
value: (object
|
||||
(pair
|
||||
key: (property_identifier) @declaration.name) @declaration.property))
|
||||
|
||||
(variable_declarator
|
||||
name: (identifier)
|
||||
value: (call_expression
|
||||
function: (member_expression
|
||||
object: (identifier) @_ts.identity.obj
|
||||
property: (property_identifier) @_ts.identity.fn)
|
||||
arguments: (arguments
|
||||
(object
|
||||
(pair
|
||||
key: (property_identifier) @declaration.name) @declaration.property)))
|
||||
(#eq? @_ts.identity.obj "Object")
|
||||
(#match? @_ts.identity.fn "^(freeze|seal|preventExtensions)$"))
|
||||
|
||||
(type_alias_declaration
|
||||
value: (object_type
|
||||
(property_signature
|
||||
|
|
|
|||
|
|
@ -404,6 +404,39 @@ export const TYPESCRIPT_QUERIES = `
|
|||
(public_field_definition
|
||||
name: (property_identifier) @name) @definition.property
|
||||
|
||||
; Object-literal keys of a NAMED object, and the same shape behind an
|
||||
; identity-preserving wrapper. Both rules existed only in JAVASCRIPT_QUERIES, so
|
||||
; a .ts file writing the single most common config idiom in the language —
|
||||
; const CONFIG = { retries: 3 } — minted no node for any key: context() answered
|
||||
; "Symbol not found" and a precise read through the holding variable had nothing
|
||||
; to resolve to.
|
||||
;
|
||||
; TypeScript sets fieldFallbackOnMethodLookup:false, so these do NOT gain
|
||||
; name-based inference; they gain the PRECISE path, which is the one TypeScript
|
||||
; is supposed to use. A read through an untyped receiver stays unresolved, and
|
||||
; is now reported as such rather than answering an empty set.
|
||||
;
|
||||
; Scoped exactly as the JavaScript rules are: bound to a variable, and for the
|
||||
; wrapper only the three functions that return the argument they were given.
|
||||
(variable_declarator
|
||||
name: (identifier)
|
||||
value: (object
|
||||
(pair
|
||||
key: (property_identifier) @name) @definition.property))
|
||||
|
||||
(variable_declarator
|
||||
name: (identifier)
|
||||
value: (call_expression
|
||||
function: (member_expression
|
||||
object: (identifier) @_ts.identity.obj
|
||||
property: (property_identifier) @_ts.identity.fn)
|
||||
arguments: (arguments
|
||||
(object
|
||||
(pair
|
||||
key: (property_identifier) @name) @definition.property)))
|
||||
(#eq? @_ts.identity.obj "Object")
|
||||
(#match? @_ts.identity.fn "^(freeze|seal|preventExtensions)$"))
|
||||
|
||||
; Private class fields: #address: Address
|
||||
(public_field_definition
|
||||
name: (private_property_identifier) @name) @definition.property
|
||||
|
|
|
|||
|
|
@ -277,7 +277,19 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j
|
|||
// still serves ParsedFiles from the durable store, so it does not substitute
|
||||
// for this bump.
|
||||
// RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING.
|
||||
const SCHEMA_BUMP = 47;
|
||||
//
|
||||
// 47 -> 48 for the TypeScript object-literal captures (R3-3): named
|
||||
// object-literal keys and the identity-wrapper form now mint `@definition.property`
|
||||
// in TYPESCRIPT_QUERIES, as they already did for JavaScript. Parse-time, so a
|
||||
// warm cache would replay ParsedFiles carrying none of those matches and the
|
||||
// keys would stay invisible.
|
||||
//
|
||||
// #2860 adds a CI check comparing this against the base branch — the merge-time
|
||||
// re-check this ledger has asked for by hand across ten entries and four exact
|
||||
// clashes. It is NOT on this branch, so until that one merges the re-check
|
||||
// below is still manual.
|
||||
// RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING.
|
||||
const SCHEMA_BUMP = 48;
|
||||
const GITNEXUS_PKG_VERSION = (() => {
|
||||
try {
|
||||
// package.json sits at gitnexus/package.json — two levels up from
|
||||
|
|
|
|||
24
gitnexus/test/fixtures/lang-resolution/typescript-alias-fields/config.ts
vendored
Normal file
24
gitnexus/test/fixtures/lang-resolution/typescript-alias-fields/config.ts
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// R3-3: the single most common config idiom in TypeScript. Both the named
|
||||
// object-literal rule and the identity-wrapper rule were JAVASCRIPT_QUERIES
|
||||
// only, so none of these keys minted a node: `context()` answered "Symbol not
|
||||
// found" and a precise read through the holding variable had nothing to
|
||||
// resolve to.
|
||||
export const tsRuntimeConfig = {
|
||||
tsConfigRetries: 3,
|
||||
tsConfigTimeoutMs: 500,
|
||||
};
|
||||
|
||||
export const TS_FROZEN_LIMITS = Object.freeze({
|
||||
tsFrozenMaxNotional: 100,
|
||||
});
|
||||
|
||||
// The PRECISE path — the one TypeScript is supposed to use. The receiver is
|
||||
// the holding variable, so this needs no name inference.
|
||||
export function readsTsConfig(): number {
|
||||
return tsRuntimeConfig.tsConfigRetries + TS_FROZEN_LIMITS.tsFrozenMaxNotional;
|
||||
}
|
||||
|
||||
// NEGATIVE CONTROL, same allowlist bound as the JavaScript rule: a non-identity
|
||||
// call returns a value of its own, so the literal's keys are arguments rather
|
||||
// than members of the binding.
|
||||
export const tsMapped = Object.entries({ tsNotAMember: 1 });
|
||||
|
|
@ -136,6 +136,40 @@ describe('TypeScript type-alias and interface members (A4)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// R3-3. Found while building a fixture for the opt-out reporting change, not
|
||||
// from a report: the object-literal rules were JavaScript-only, so the most
|
||||
// common config idiom in TypeScript had invisible keys.
|
||||
describe('object-literal keys in TypeScript (R3-3)', () => {
|
||||
const names = (): string[] =>
|
||||
Array.from(
|
||||
(result as unknown as { graph: { iterNodes(): Iterable<PropNode> } }).graph.iterNodes(),
|
||||
)
|
||||
.filter((n) => n.label === 'Property')
|
||||
.map((n) => String(n.properties.name));
|
||||
|
||||
it('indexes keys of a named object literal', () => {
|
||||
expect(names()).toContain('tsConfigRetries');
|
||||
expect(names()).toContain('tsConfigTimeoutMs');
|
||||
});
|
||||
|
||||
it('indexes keys behind an identity-preserving wrapper', () => {
|
||||
expect(names()).toContain('tsFrozenMaxNotional');
|
||||
});
|
||||
|
||||
// The half that matters for TypeScript specifically. It opts out of
|
||||
// name inference, so the value of minting these nodes is that the PRECISE
|
||||
// path — a read through the holding variable — now has something to
|
||||
// resolve to.
|
||||
it('resolves a precise read through the holding variable', () => {
|
||||
expect(readersOf('tsConfigRetries')).toContain('readsTsConfig');
|
||||
expect(readersOf('tsFrozenMaxNotional')).toContain('readsTsConfig');
|
||||
});
|
||||
|
||||
it('keeps the same allowlist bound as the JavaScript rule', () => {
|
||||
expect(names()).not.toContain('tsNotAMember');
|
||||
});
|
||||
});
|
||||
|
||||
describe('type consumers (R2-2)', () => {
|
||||
const usersOf = (typeName: string): string[] =>
|
||||
getRelationships(result, 'USES')
|
||||
|
|
|
|||
|
|
@ -141,8 +141,8 @@ describe('PARSE_CACHE_VERSION', () => {
|
|||
// a row. Same lesson as the note above — the pin cannot detect the tie, since
|
||||
// both sides asserted `toBe(45)` and that passes while main is already 45.
|
||||
// Only the merge-time diff against origin/main surfaces it.
|
||||
it('pins SCHEMA_BUMP to 47 so concurrent bumps cannot silently collide (#2766)', () => {
|
||||
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(47);
|
||||
it('pins SCHEMA_BUMP to 48 so concurrent bumps cannot silently collide (#2766)', () => {
|
||||
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(48);
|
||||
});
|
||||
|
||||
it('embeds the gitnexus package version (so upgrades invalidate the cache)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue