mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +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
|
(property_signature
|
||||||
name: (property_identifier) @declaration.name) @declaration.property)
|
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
|
(type_alias_declaration
|
||||||
value: (object_type
|
value: (object_type
|
||||||
(property_signature
|
(property_signature
|
||||||
|
|
|
||||||
|
|
@ -404,6 +404,39 @@ export const TYPESCRIPT_QUERIES = `
|
||||||
(public_field_definition
|
(public_field_definition
|
||||||
name: (property_identifier) @name) @definition.property
|
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
|
; Private class fields: #address: Address
|
||||||
(public_field_definition
|
(public_field_definition
|
||||||
name: (private_property_identifier) @name) @definition.property
|
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
|
// still serves ParsedFiles from the durable store, so it does not substitute
|
||||||
// for this bump.
|
// for this bump.
|
||||||
// RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING.
|
// 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 = (() => {
|
const GITNEXUS_PKG_VERSION = (() => {
|
||||||
try {
|
try {
|
||||||
// package.json sits at gitnexus/package.json — two levels up from
|
// 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)', () => {
|
describe('type consumers (R2-2)', () => {
|
||||||
const usersOf = (typeName: string): string[] =>
|
const usersOf = (typeName: string): string[] =>
|
||||||
getRelationships(result, 'USES')
|
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
|
// 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.
|
// both sides asserted `toBe(45)` and that passes while main is already 45.
|
||||||
// Only the merge-time diff against origin/main surfaces it.
|
// Only the merge-time diff against origin/main surfaces it.
|
||||||
it('pins SCHEMA_BUMP to 47 so concurrent bumps cannot silently collide (#2766)', () => {
|
it('pins SCHEMA_BUMP to 48 so concurrent bumps cannot silently collide (#2766)', () => {
|
||||||
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(47);
|
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(48);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('embeds the gitnexus package version (so upgrades invalidate the cache)', () => {
|
it('embeds the gitnexus package version (so upgrades invalidate the cache)', () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue