mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat(scope-resolution): anchor anonymous returned object literals to their function
The last gap round 3 named, and the dominant shape in idiomatic JS: 437
`return {` sites in a single backend directory of the reporting repo, including
the ~25-field payload of its entire signal pipeline. The literal binds to
nothing, so its keys could not even be named — "who reads wickRatio?" had no
symbol to ask about.
The enclosing FUNCTION is the owner: the literal is that function's return
shape, a contract its callers consume. Keys qualify as `<function>.<key>`, so
two functions returning the same name stay two shapes rather than one merged
symbol, and multiple returns in one function stay distinct by position.
RECONCILING THIS WITH R2-1b, which deliberately modelled returned keys as WRITES
to avoid adding same-named competitors to narrowing. These are definitions, but
narrowing now ranks DECLARED anchors — named literals, class fields, interface
and alias members — strictly above return shapes. A name that already resolved
keeps resolving to what it resolved to before, so the competitor problem R2-1b
was avoiding cannot come back. Mutation-checked: dropping that ranking breaks
five pre-existing R2 resolutions.
That also required an R2-1b assertion to change, and the change is a
strengthening rather than a concession. It asserted `toHaveLength(1)` — no new
definition — as a proxy for "adding definitions must not move an existing
answer". The proxy is now false while the property still holds, so the property
itself is asserted directly.
No `HAS_PROPERTY` edge from the function: that would be a `Function|Property`
relation pair the schema does not declare, and an undeclared pair does not
degrade — it throws and kills the whole analyze. That already shipped once in
this PR.
Two things found by dumping rather than assuming, both fixed here:
SHORTHAND keys were not matched at all. `return { symbol, interval, score }` is
the commonest spelling and the reporting repo's own payload is mostly this form,
but tree-sitter models it as `shorthand_property_identifier`, which `(pair)`
does not match. Caught by dumping the golden fixture and seeing a literal
returning `{ level, message, timestamp: Date.now() }` had indexed only
`timestamp`. Now covered in return position AND in the variable-bound rule,
which had the same gap.
Provenance was flagged by owner-presence, which mislabelled the anonymous case:
a callback's return shape yields no name to qualify by, so it looked like a
DECLARED anchor and would have outranked real declarations. Flagged by position
now — a different question from whether a name could be derived.
SCHEMA_BUMP 48 -> 49. Within one PR the version only has to differ from main's,
but a build stamped 48 was installed and used to analyze before these captures
existed, so caches stamped 48 carry none of them — the intermediate-build hazard
this ledger already records for 33/34.
Golden regenerated after verifying the drift: exactly +10 Property and +10
DEFINES, every pre-existing count unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8972d223a9
commit
af5eec5c05
11 changed files with 348 additions and 22 deletions
|
|
@ -101,6 +101,19 @@ const OVERSATURATED = null;
|
|||
interface PropertyCandidate {
|
||||
readonly id: string;
|
||||
readonly filePath: string;
|
||||
/**
|
||||
* True when this definition is the RETURN SHAPE of a function (R3-4) rather
|
||||
* than a declared surface — a named object literal, a class field, an
|
||||
* interface or alias member.
|
||||
*
|
||||
* Return shapes are the weaker anchor and are ranked below declared ones, so
|
||||
* adding them cannot change an answer that already resolved. That is what
|
||||
* reconciles this with R2-1b, which deliberately modelled returned keys as
|
||||
* WRITES to avoid adding same-named competitors to narrowing: they are
|
||||
* definitions now, but they never outrank a real declaration, so the
|
||||
* competitor problem it was avoiding does not come back.
|
||||
*/
|
||||
readonly fromReturnShape: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -186,13 +199,18 @@ export function buildPropertyNameIndex(graph: KnowledgeGraph): PropertyNameIndex
|
|||
if (typeof name !== 'string' || name.length === 0) continue;
|
||||
const filePath = node.properties.filePath;
|
||||
if (typeof filePath !== 'string') continue;
|
||||
const candidate: PropertyCandidate = {
|
||||
id: node.id,
|
||||
filePath,
|
||||
fromReturnShape: node.properties.fromReturnShape === true,
|
||||
};
|
||||
const existing = byName.get(name);
|
||||
if (existing === undefined) {
|
||||
byName.set(name, [{ id: node.id, filePath }]);
|
||||
byName.set(name, [candidate]);
|
||||
continue;
|
||||
}
|
||||
if (existing.some((c) => c.id === node.id)) continue;
|
||||
existing.push({ id: node.id, filePath });
|
||||
existing.push(candidate);
|
||||
}
|
||||
return byName;
|
||||
}
|
||||
|
|
@ -281,13 +299,24 @@ function buildDirectImportMap(
|
|||
* imported third would answer a question the reader's own file contradicts.
|
||||
*/
|
||||
function narrowToSingleCandidate(
|
||||
candidates: readonly PropertyCandidate[],
|
||||
candidatesIn: readonly PropertyCandidate[],
|
||||
readingFile: string,
|
||||
importedFiles: ReadonlySet<string> | undefined,
|
||||
): { readonly id: string; readonly tier: string } | null {
|
||||
if (candidates.length === 1) {
|
||||
return { id: candidates[0]!.id, tier: 'workspace-unique' };
|
||||
// DECLARED ANCHORS FIRST. A return shape is a real definition but a weaker
|
||||
// one: it says "some function builds an object with this key", where a named
|
||||
// literal or a class/interface member says "this IS the field". Whenever both
|
||||
// exist, the declared one is what a reader means — and ranking it first is
|
||||
// what guarantees R3-4 cannot change an answer that already resolved before
|
||||
// return shapes were indexed at all.
|
||||
let candidates = candidatesIn;
|
||||
const declared = candidates.filter((c) => !c.fromReturnShape);
|
||||
const ranked = declared.length > 0 ? declared : candidates;
|
||||
|
||||
if (ranked.length === 1) {
|
||||
return { id: ranked[0]!.id, tier: 'workspace-unique' };
|
||||
}
|
||||
candidates = ranked;
|
||||
|
||||
const sameFile = candidates.filter((c) => c.filePath === readingFile);
|
||||
if (sameFile.length > 0) {
|
||||
|
|
|
|||
|
|
@ -424,6 +424,43 @@ export const TYPESCRIPT_QUERIES = `
|
|||
(pair
|
||||
key: (property_identifier) @name) @definition.property))
|
||||
|
||||
; Keys of an ANONYMOUS object literal in RETURN position (R3-4). The dominant
|
||||
; shape in idiomatic JS: 437 sites in one backend directory of the reporting
|
||||
; repo, including the ~25-field payload of its whole signal pipeline, none of
|
||||
; which could be named because the literal binds to nothing.
|
||||
;
|
||||
; The enclosing function is the owner -- the literal is that function's return
|
||||
; shape, a contract its callers consume -- so the key qualifies as
|
||||
; <function>.<key> and two functions returning the same key stay distinct.
|
||||
;
|
||||
; DEFINITIONS, unlike the record-construction writes of R2-1b, and the
|
||||
; difference is deliberate: there a definition already existed elsewhere and a
|
||||
; construction site was a USE of it, while here nothing else names the field at
|
||||
; all. To keep that from regressing R2-1b's case, narrowing ranks declared
|
||||
; anchors ABOVE return shapes, so a name that already resolves keeps resolving
|
||||
; to what it resolved to before.
|
||||
(return_statement
|
||||
(object
|
||||
(pair
|
||||
key: (property_identifier) @name) @definition.property))
|
||||
|
||||
; SHORTHAND keys of the same literal. "return { symbol, interval, score }" is
|
||||
; the commonest spelling of all -- the reporting repo's own alert payload is
|
||||
; mostly shorthand -- and (pair) does not match it: tree-sitter models it as
|
||||
; shorthand_property_identifier, where the key IS the value. Found by dumping
|
||||
; the golden fixture and noticing that a literal returning
|
||||
; { level, message, timestamp: Date.now() } had indexed only timestamp.
|
||||
(return_statement
|
||||
(object
|
||||
(shorthand_property_identifier) @name @definition.property))
|
||||
|
||||
; Shorthand keys of a named object literal -- same gap, same reason as the
|
||||
; return-position rule above.
|
||||
(variable_declarator
|
||||
name: (identifier)
|
||||
value: (object
|
||||
(shorthand_property_identifier) @name @definition.property))
|
||||
|
||||
(variable_declarator
|
||||
name: (identifier)
|
||||
value: (call_expression
|
||||
|
|
@ -932,6 +969,43 @@ export const JAVASCRIPT_QUERIES = `
|
|||
(pair
|
||||
key: (property_identifier) @name) @definition.property))
|
||||
|
||||
; Keys of an ANONYMOUS object literal in RETURN position (R3-4). The dominant
|
||||
; shape in idiomatic JS: 437 sites in one backend directory of the reporting
|
||||
; repo, including the ~25-field payload of its whole signal pipeline, none of
|
||||
; which could be named because the literal binds to nothing.
|
||||
;
|
||||
; The enclosing function is the owner -- the literal is that function's return
|
||||
; shape, a contract its callers consume -- so the key qualifies as
|
||||
; <function>.<key> and two functions returning the same key stay distinct.
|
||||
;
|
||||
; DEFINITIONS, unlike the record-construction writes of R2-1b, and the
|
||||
; difference is deliberate: there a definition already existed elsewhere and a
|
||||
; construction site was a USE of it, while here nothing else names the field at
|
||||
; all. To keep that from regressing R2-1b's case, narrowing ranks declared
|
||||
; anchors ABOVE return shapes, so a name that already resolves keeps resolving
|
||||
; to what it resolved to before.
|
||||
(return_statement
|
||||
(object
|
||||
(pair
|
||||
key: (property_identifier) @name) @definition.property))
|
||||
|
||||
; SHORTHAND keys of the same literal. "return { symbol, interval, score }" is
|
||||
; the commonest spelling of all -- the reporting repo's own alert payload is
|
||||
; mostly shorthand -- and (pair) does not match it: tree-sitter models it as
|
||||
; shorthand_property_identifier, where the key IS the value. Found by dumping
|
||||
; the golden fixture and noticing that a literal returning
|
||||
; { level, message, timestamp: Date.now() } had indexed only timestamp.
|
||||
(return_statement
|
||||
(object
|
||||
(shorthand_property_identifier) @name @definition.property))
|
||||
|
||||
; Shorthand keys of a named object literal -- same gap, same reason as the
|
||||
; return-position rule above.
|
||||
(variable_declarator
|
||||
name: (identifier)
|
||||
value: (object
|
||||
(shorthand_property_identifier) @name @definition.property))
|
||||
|
||||
; Same named shape, behind an IDENTITY-PRESERVING wrapper (R2-1a):
|
||||
;
|
||||
; export const INERT_EXIT_CONTRACT = Object.freeze({ exitModel: 'bracket', ... });
|
||||
|
|
|
|||
|
|
@ -1177,6 +1177,91 @@ const BLOCK_SCOPE_BOUNDARY_TYPES = new Set([
|
|||
* ancestor also returns null (catches block-scoped declarations inside
|
||||
* top-level `if`/`for`/`try`/etc., which cannot be imported).
|
||||
*/
|
||||
/**
|
||||
* Owner for the keys of an ANONYMOUS object literal in return position (R3-4).
|
||||
*
|
||||
* `return { symbol, score, wickRatio, … }` binds to nothing, so its keys had no
|
||||
* anchor and could not be qualified — which on the reporting repo left the
|
||||
* central payload of the signal pipeline, ~25 fields, entirely unqueryable.
|
||||
* There are 437 such sites in one backend directory, so this is the dominant
|
||||
* shape, not an edge case.
|
||||
*
|
||||
* The enclosing FUNCTION is the honest owner: the literal is that function's
|
||||
* return shape, which is a contract its callers consume. Qualifying by it keeps
|
||||
* two functions returning the same key name as two distinct nodes, exactly as
|
||||
* `ownerName` does for variable-bound literals.
|
||||
*
|
||||
* Returns null when the literal is not DIRECTLY returned (a nested literal, or
|
||||
* one inside a callback several frames down), because then the enclosing
|
||||
* function is not what the object describes.
|
||||
*/
|
||||
/**
|
||||
* True when this definition node is a key of a literal in RETURN position.
|
||||
*
|
||||
* Deliberately independent of whether an OWNER NAME could be derived. The two
|
||||
* are different questions, and conflating them mislabels the anonymous case:
|
||||
* `[function (row) { return { k: row.x }; }]` yields no name to qualify by, so
|
||||
* the owner lookup returns null — but the key is still a return shape, and
|
||||
* flagging it by owner-presence would leave it looking like a DECLARED anchor
|
||||
* and let it outrank a real declaration during narrowing.
|
||||
*/
|
||||
export const isReturnShapeProperty = (node: SyntaxNode): boolean => {
|
||||
let current: SyntaxNode | null = node;
|
||||
let objectDepth = 0;
|
||||
while (current && objectDepth === 0) {
|
||||
if (current.type === 'object') objectDepth = 1;
|
||||
else if (FUNCTION_NODE_TYPES.has(current.type)) return false;
|
||||
else current = current.parent;
|
||||
}
|
||||
return current?.parent?.type === 'return_statement';
|
||||
};
|
||||
|
||||
export const findReturnShapeOwnerInfo = (
|
||||
node: SyntaxNode,
|
||||
filePath: string,
|
||||
// NO `ownerId`, deliberately, and the union's optional field is what says so.
|
||||
// An owner id would emit `HAS_PROPERTY` from the FUNCTION, a `Function|Property`
|
||||
// relation pair that the schema does not declare — and an undeclared pair does
|
||||
// not degrade, it throws `UndeclaredRelationPairError` and kills the entire
|
||||
// analyze. That already shipped once in this PR. The qualifier alone is what
|
||||
// this needs: it makes the key nameable and keeps two functions' same-named
|
||||
// keys distinct, without asserting a containment edge nothing consumes.
|
||||
): { readonly ownerId?: string; readonly ownerName: string } | null => {
|
||||
// Walk to the literal this key belongs to; bail if it is nested inside
|
||||
// another object, whose shape it describes instead.
|
||||
let current: SyntaxNode | null = node;
|
||||
let objectDepth = 0;
|
||||
while (current && objectDepth === 0) {
|
||||
if (current.type === 'object') objectDepth = 1;
|
||||
else if (FUNCTION_NODE_TYPES.has(current.type)) return null;
|
||||
else current = current.parent;
|
||||
}
|
||||
if (!current) return null;
|
||||
const literal = current;
|
||||
if (literal.parent?.type !== 'return_statement') return null;
|
||||
|
||||
// The nearest enclosing function-like, and its name. An anonymous function
|
||||
// (a callback, an IIFE) gives nothing to qualify by, so those stay
|
||||
// unanchored rather than colliding on a shared empty owner.
|
||||
let fn: SyntaxNode | null = literal.parent.parent;
|
||||
while (fn && !FUNCTION_NODE_TYPES.has(fn.type)) fn = fn.parent;
|
||||
if (!fn) return null;
|
||||
|
||||
const nameNode = fn.childForFieldName?.('name');
|
||||
if (nameNode?.type === 'identifier' || nameNode?.type === 'property_identifier') {
|
||||
return { ownerName: nameNode.text };
|
||||
}
|
||||
// `const formatAlert = (…) => ({ … })` and `const f = function () {}`: the
|
||||
// name is on the declarator, not the function.
|
||||
const declarator = fn.parent;
|
||||
if (declarator?.type === 'variable_declarator') {
|
||||
const declName = declarator.childForFieldName?.('name');
|
||||
if (declName?.type === 'identifier') return { ownerName: declName.text };
|
||||
}
|
||||
void filePath;
|
||||
return null;
|
||||
};
|
||||
|
||||
export const findObjectLiteralBindingInfo = (
|
||||
node: SyntaxNode,
|
||||
filePath: string,
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ import {
|
|||
getDefinitionNodeFromCaptures,
|
||||
findEnclosingClassInfo,
|
||||
findObjectLiteralBindingInfo,
|
||||
findReturnShapeOwnerInfo,
|
||||
isReturnShapeProperty,
|
||||
findMemberAssignmentOwnerInfo,
|
||||
isCjsDefaultExportAssignment,
|
||||
type EnclosingClassInfo,
|
||||
|
|
@ -2361,8 +2363,19 @@ const processFileGroup = (
|
|||
// byte-identical or every object-literal method in every indexed
|
||||
// repo changes id.
|
||||
includeOwnerName: nodeLabel === 'Property',
|
||||
}))
|
||||
}) ??
|
||||
// R3-4: an anonymous literal in return position is owned by the
|
||||
// function whose shape it is. Last in the chain so a variable-bound
|
||||
// literal keeps its existing owner and its existing id.
|
||||
(nodeLabel === 'Property' ? findReturnShapeOwnerInfo(definitionNode, file.path) : null))
|
||||
: null;
|
||||
// Provenance for narrowing (R3-4). A return shape is a real definition but
|
||||
// the weaker one, and the unique-name pass ranks declared anchors above it
|
||||
// so indexing these cannot change an answer that already resolved.
|
||||
const returnShapeProperty =
|
||||
nodeLabel === 'Property' && definitionNode !== undefined && definitionNode !== null
|
||||
? isReturnShapeProperty(definitionNode)
|
||||
: false;
|
||||
|
||||
// #1978: hoisted ABOVE qualifiedName/node-id (load-bearing order) so a
|
||||
// class-like node can key its id by its fully-qualified path. Derived from
|
||||
|
|
@ -2809,6 +2822,7 @@ const processFileGroup = (
|
|||
...(description !== undefined ? { description } : {}),
|
||||
...methodProps,
|
||||
...(declaredType !== undefined ? { declaredType } : {}),
|
||||
...(returnShapeProperty ? { fromReturnShape: true } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -289,7 +289,18 @@ import type { ParseWorkerResult } from '../core/ingestion/workers/parse-worker.j
|
|||
// 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;
|
||||
// 48 -> 49 for the return-shape and shorthand captures (R3-4): keys of an
|
||||
// anonymous literal in return position, and shorthand keys in both that and the
|
||||
// variable-bound form. Parse-time again.
|
||||
//
|
||||
// The v34 hazard, and this branch has already tripped it: a build stamped 48
|
||||
// was installed and used to analyze two repos BEFORE these captures existed, so
|
||||
// caches stamped 48 exist that carry none of them. Within one PR the version
|
||||
// only has to differ from main's, but an INTERMEDIATE build of the same series
|
||||
// is a different capture set wearing the same number — which is exactly what
|
||||
// the note above records for 33/34.
|
||||
// RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING.
|
||||
const SCHEMA_BUMP = 49;
|
||||
const GITNEXUS_PKG_VERSION = (() => {
|
||||
try {
|
||||
// package.json sits at gitnexus/package.json — two levels up from
|
||||
|
|
|
|||
47
gitnexus/test/fixtures/lang-resolution/javascript-object-properties/return-shape.js
vendored
Normal file
47
gitnexus/test/fixtures/lang-resolution/javascript-object-properties/return-shape.js
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// R3-4: an anonymous literal in return position — the dominant shape in
|
||||
// idiomatic JS (437 sites in one backend directory of the reporting repo),
|
||||
// including the ~25-field payload of its entire signal pipeline. It binds to
|
||||
// nothing, so its keys had no anchor and could not be named at all.
|
||||
export function formatAlert(row) {
|
||||
const shorthandOnlyField = row.shorthand;
|
||||
return {
|
||||
returnShapeOnlyField: row.raw,
|
||||
sharedWithDeclared: row.other,
|
||||
// SHORTHAND — the commonest spelling, and the one `(pair)` cannot match.
|
||||
// The reporting repo's own alert payload is mostly this form.
|
||||
shorthandOnlyField,
|
||||
};
|
||||
}
|
||||
|
||||
// A SECOND function returning a same-named key. Two distinct shapes, so two
|
||||
// distinct nodes — qualifying by the owning function is what keeps them apart.
|
||||
export function formatSummary(row) {
|
||||
return {
|
||||
summaryOnlyField: row.summary,
|
||||
};
|
||||
}
|
||||
|
||||
// The reader. Untyped receiver, so this is the name-inference path.
|
||||
export function readsReturnShape(alert) {
|
||||
return alert.returnShapeOnlyField;
|
||||
}
|
||||
|
||||
// The R2-1b GUARANTEE, as a fixture: a DECLARED anchor for the same name.
|
||||
// `sharedWithDeclared` is both a named-object key and a return-shape key, and a
|
||||
// read of it must keep resolving to the DECLARED one — otherwise indexing
|
||||
// return shapes would silently move existing answers.
|
||||
export const declaredHome = {
|
||||
sharedWithDeclared: 1,
|
||||
};
|
||||
|
||||
export function readsShared(bag) {
|
||||
return bag.sharedWithDeclared;
|
||||
}
|
||||
|
||||
// Anonymous functions give nothing to qualify by, so their return shapes stay
|
||||
// unanchored rather than colliding on a shared empty owner.
|
||||
export const anonHolder = [
|
||||
function (row) {
|
||||
return { anonReturnKey: row.x };
|
||||
},
|
||||
];
|
||||
|
|
@ -44,6 +44,11 @@ export type NestedConfig = {
|
|||
};
|
||||
|
||||
// Inline RETURN type — the third position the unanchored rule reached.
|
||||
export function buildInline(): { inlineReturnOnlyKey: number } {
|
||||
return { inlineReturnOnlyKey: 1 };
|
||||
// The TYPE annotation's member and the returned VALUE's key are named
|
||||
// differently ON PURPOSE. They are separate rules with opposite expectations —
|
||||
// an inline return TYPE must mint nothing (RV-4), while a returned literal's
|
||||
// keys are a function's return shape and must mint (R3-4) — and sharing a name
|
||||
// left the RV-4 assertion unable to tell which rule produced the node.
|
||||
export function buildInline(): { inlineReturnTypeOnlyKey: number } {
|
||||
return { inlineReturnValueOnlyKey: 1 } as never;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"capture": "initial capture (U8, post-U1–U7)",
|
||||
"fixture": "mini-repo",
|
||||
"totalFileCount": 7,
|
||||
"symbols": 41,
|
||||
"relationships": 85,
|
||||
"symbols": 51,
|
||||
"relationships": 95,
|
||||
"processes": 4,
|
||||
"byType": {
|
||||
"Class": 1,
|
||||
|
|
@ -14,13 +14,13 @@
|
|||
"Interface": 3,
|
||||
"Method": 1,
|
||||
"Process": 4,
|
||||
"Property": 8
|
||||
"Property": 18
|
||||
},
|
||||
"byRelType": {
|
||||
"ACCESSES": 3,
|
||||
"CALLS": 9,
|
||||
"CONTAINS": 7,
|
||||
"DEFINES": 16,
|
||||
"DEFINES": 26,
|
||||
"HAS_METHOD": 1,
|
||||
"HAS_PROPERTY": 8,
|
||||
"IMPORTS": 12,
|
||||
|
|
@ -28,5 +28,5 @@
|
|||
"STEP_IN_PROCESS": 12,
|
||||
"USES": 5
|
||||
},
|
||||
"edgeDigest": "c55bd8307a5fccbfd23d5aa93f241e26ef0f45c1a9e57691f2bf4f6602e8e4ab"
|
||||
"edgeDigest": "5dddd1f466deeda1197eb61b480a4f3a5da67dfc0d00ace22377158366e87d00"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,11 +145,21 @@ describe('JavaScript plain-object property access (A1/A5)', () => {
|
|||
expect(writersOf('destructuredOnlyField')).toContain('buildFlat');
|
||||
});
|
||||
|
||||
// The point of modelling these as writes rather than definitions: more
|
||||
// definitions would add same-named competitors to the narrowing that makes
|
||||
// these fields resolvable in the first place.
|
||||
it('mints NO new definition for a constructed record', () => {
|
||||
expect(propertyNames().filter((n) => n === 'destructuredOnlyField')).toHaveLength(1);
|
||||
// This asserted `toHaveLength(1)` — no new definition — until R3-4 began
|
||||
// anchoring returned literals, which mints exactly one here (`buildFlat`'s
|
||||
// return shape). The assertion was the right instinct expressed as the
|
||||
// wrong invariant: what R2-1b actually protects is that adding definitions
|
||||
// must not move an answer that already resolved, and node count was a proxy
|
||||
// for that. The property itself is now asserted directly, and it holds
|
||||
// because narrowing ranks declared anchors above return shapes.
|
||||
it('keeps the DECLARED definition winning despite a return-shape sibling', () => {
|
||||
const nodes = propertyNames().filter((n) => n === 'destructuredOnlyField');
|
||||
expect(nodes.length).toBeGreaterThan(1);
|
||||
// Every reader still resolves, and to the declared home — a read that had
|
||||
// dropped to ambiguous would show up as a missing edge here.
|
||||
for (const reader of ['appliesDestructured', 'appliesShorthand', 'appliesRenamed']) {
|
||||
expect(readersOf('destructuredOnlyField')).toContain(reader);
|
||||
}
|
||||
});
|
||||
|
||||
it('leaves an inline call-argument prop bag alone', () => {
|
||||
|
|
@ -158,6 +168,54 @@ describe('JavaScript plain-object property access (A1/A5)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// R3-4. The dominant shape in idiomatic JS and the one with no anchor at all:
|
||||
// 437 `return {` sites in a single backend directory of the reporting repo,
|
||||
// including the ~25-field payload of its whole signal pipeline. The literal
|
||||
// binds to nothing, so its keys could not even be named.
|
||||
describe('anonymous returned object literals (R3-4)', () => {
|
||||
it('indexes keys of a literal returned from a named function', () => {
|
||||
expect(propertyNames()).toContain('returnShapeOnlyField');
|
||||
});
|
||||
|
||||
it('resolves a read of a return-shape key', () => {
|
||||
expect(readersOf('returnShapeOnlyField')).toContain('readsReturnShape');
|
||||
});
|
||||
|
||||
// `{ symbol, interval, score }` is the commonest spelling of all and
|
||||
// `(pair)` does not match it — tree-sitter models it as
|
||||
// `shorthand_property_identifier`, where the key IS the value. Caught by
|
||||
// dumping the golden fixture and seeing that a literal returning
|
||||
// `{ level, message, timestamp: Date.now() }` had indexed only `timestamp`.
|
||||
it('indexes SHORTHAND keys, not just explicit pairs', () => {
|
||||
expect(propertyNames()).toContain('shorthandOnlyField');
|
||||
});
|
||||
|
||||
// Qualified by the owning function, so two functions returning the same key
|
||||
// are two shapes rather than one merged symbol — the same collision
|
||||
// `ownerName` prevents for variable-bound literals.
|
||||
it('qualifies by the owning function', () => {
|
||||
const ids = Array.from(
|
||||
(result as unknown as { graph: { iterNodes(): Iterable<PropNode> } }).graph.iterNodes(),
|
||||
)
|
||||
.filter((n) => n.label === 'Property')
|
||||
.map((n) => String(n.id));
|
||||
expect(ids.some((id) => id.includes('formatAlert.returnShapeOnlyField'))).toBe(true);
|
||||
expect(ids.some((id) => id.includes('formatSummary.summaryOnlyField'))).toBe(true);
|
||||
});
|
||||
|
||||
// THE GUARANTEE that reconciles this with R2-1b. `sharedWithDeclared` is
|
||||
// both a named-object key and a return-shape key; a read must still resolve
|
||||
// to the DECLARED one, or indexing return shapes would silently move
|
||||
// answers that already worked.
|
||||
it('never outranks a declared anchor', () => {
|
||||
expect(readersOf('sharedWithDeclared')).toContain('readsShared');
|
||||
const declaredWins = getRelationships(result, 'ACCESSES').filter(
|
||||
(e) => e.target === 'sharedWithDeclared' && e.source === 'readsShared',
|
||||
);
|
||||
expect(declaredWins.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -117,7 +117,10 @@ describe('TypeScript type-alias and interface members (A4)', () => {
|
|||
});
|
||||
|
||||
it('does not mint a member for an inline RETURN type', () => {
|
||||
expect(propertyIds().some((id) => id.includes('inlineReturnOnlyKey'))).toBe(false);
|
||||
// The TYPE's member, not the returned value's key — those are different
|
||||
// rules with opposite expectations, and the fixture names them apart so
|
||||
// this assertion cannot be satisfied by the wrong one.
|
||||
expect(propertyIds().some((id) => id.includes('inlineReturnTypeOnlyKey'))).toBe(false);
|
||||
});
|
||||
|
||||
// The other half: anchoring must not cost real members.
|
||||
|
|
|
|||
|
|
@ -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 48 so concurrent bumps cannot silently collide (#2766)', () => {
|
||||
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(48);
|
||||
it('pins SCHEMA_BUMP to 49 so concurrent bumps cannot silently collide (#2766)', () => {
|
||||
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(49);
|
||||
});
|
||||
|
||||
it('embeds the gitnexus package version (so upgrades invalidate the cache)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue