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 {
|
interface PropertyCandidate {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly filePath: 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;
|
if (typeof name !== 'string' || name.length === 0) continue;
|
||||||
const filePath = node.properties.filePath;
|
const filePath = node.properties.filePath;
|
||||||
if (typeof filePath !== 'string') continue;
|
if (typeof filePath !== 'string') continue;
|
||||||
|
const candidate: PropertyCandidate = {
|
||||||
|
id: node.id,
|
||||||
|
filePath,
|
||||||
|
fromReturnShape: node.properties.fromReturnShape === true,
|
||||||
|
};
|
||||||
const existing = byName.get(name);
|
const existing = byName.get(name);
|
||||||
if (existing === undefined) {
|
if (existing === undefined) {
|
||||||
byName.set(name, [{ id: node.id, filePath }]);
|
byName.set(name, [candidate]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (existing.some((c) => c.id === node.id)) continue;
|
if (existing.some((c) => c.id === node.id)) continue;
|
||||||
existing.push({ id: node.id, filePath });
|
existing.push(candidate);
|
||||||
}
|
}
|
||||||
return byName;
|
return byName;
|
||||||
}
|
}
|
||||||
|
|
@ -281,13 +299,24 @@ function buildDirectImportMap(
|
||||||
* imported third would answer a question the reader's own file contradicts.
|
* imported third would answer a question the reader's own file contradicts.
|
||||||
*/
|
*/
|
||||||
function narrowToSingleCandidate(
|
function narrowToSingleCandidate(
|
||||||
candidates: readonly PropertyCandidate[],
|
candidatesIn: readonly PropertyCandidate[],
|
||||||
readingFile: string,
|
readingFile: string,
|
||||||
importedFiles: ReadonlySet<string> | undefined,
|
importedFiles: ReadonlySet<string> | undefined,
|
||||||
): { readonly id: string; readonly tier: string } | null {
|
): { readonly id: string; readonly tier: string } | null {
|
||||||
if (candidates.length === 1) {
|
// DECLARED ANCHORS FIRST. A return shape is a real definition but a weaker
|
||||||
return { id: candidates[0]!.id, tier: 'workspace-unique' };
|
// 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);
|
const sameFile = candidates.filter((c) => c.filePath === readingFile);
|
||||||
if (sameFile.length > 0) {
|
if (sameFile.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -424,6 +424,43 @@ export const TYPESCRIPT_QUERIES = `
|
||||||
(pair
|
(pair
|
||||||
key: (property_identifier) @name) @definition.property))
|
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
|
(variable_declarator
|
||||||
name: (identifier)
|
name: (identifier)
|
||||||
value: (call_expression
|
value: (call_expression
|
||||||
|
|
@ -932,6 +969,43 @@ export const JAVASCRIPT_QUERIES = `
|
||||||
(pair
|
(pair
|
||||||
key: (property_identifier) @name) @definition.property))
|
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):
|
; Same named shape, behind an IDENTITY-PRESERVING wrapper (R2-1a):
|
||||||
;
|
;
|
||||||
; export const INERT_EXIT_CONTRACT = Object.freeze({ exitModel: 'bracket', ... });
|
; 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
|
* ancestor also returns null (catches block-scoped declarations inside
|
||||||
* top-level `if`/`for`/`try`/etc., which cannot be imported).
|
* 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 = (
|
export const findObjectLiteralBindingInfo = (
|
||||||
node: SyntaxNode,
|
node: SyntaxNode,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,8 @@ import {
|
||||||
getDefinitionNodeFromCaptures,
|
getDefinitionNodeFromCaptures,
|
||||||
findEnclosingClassInfo,
|
findEnclosingClassInfo,
|
||||||
findObjectLiteralBindingInfo,
|
findObjectLiteralBindingInfo,
|
||||||
|
findReturnShapeOwnerInfo,
|
||||||
|
isReturnShapeProperty,
|
||||||
findMemberAssignmentOwnerInfo,
|
findMemberAssignmentOwnerInfo,
|
||||||
isCjsDefaultExportAssignment,
|
isCjsDefaultExportAssignment,
|
||||||
type EnclosingClassInfo,
|
type EnclosingClassInfo,
|
||||||
|
|
@ -2361,8 +2363,19 @@ const processFileGroup = (
|
||||||
// byte-identical or every object-literal method in every indexed
|
// byte-identical or every object-literal method in every indexed
|
||||||
// repo changes id.
|
// repo changes id.
|
||||||
includeOwnerName: nodeLabel === 'Property',
|
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;
|
: 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
|
// #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
|
// class-like node can key its id by its fully-qualified path. Derived from
|
||||||
|
|
@ -2809,6 +2822,7 @@ const processFileGroup = (
|
||||||
...(description !== undefined ? { description } : {}),
|
...(description !== undefined ? { description } : {}),
|
||||||
...methodProps,
|
...methodProps,
|
||||||
...(declaredType !== undefined ? { declaredType } : {}),
|
...(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
|
// clashes. It is NOT on this branch, so until that one merges the re-check
|
||||||
// below is still manual.
|
// below is still manual.
|
||||||
// RE-CHECK AGAINST origin/main IMMEDIATELY BEFORE MERGING.
|
// 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 = (() => {
|
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
|
||||||
|
|
|
||||||
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.
|
// Inline RETURN type — the third position the unanchored rule reached.
|
||||||
export function buildInline(): { inlineReturnOnlyKey: number } {
|
// The TYPE annotation's member and the returned VALUE's key are named
|
||||||
return { inlineReturnOnlyKey: 1 };
|
// 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)",
|
"capture": "initial capture (U8, post-U1–U7)",
|
||||||
"fixture": "mini-repo",
|
"fixture": "mini-repo",
|
||||||
"totalFileCount": 7,
|
"totalFileCount": 7,
|
||||||
"symbols": 41,
|
"symbols": 51,
|
||||||
"relationships": 85,
|
"relationships": 95,
|
||||||
"processes": 4,
|
"processes": 4,
|
||||||
"byType": {
|
"byType": {
|
||||||
"Class": 1,
|
"Class": 1,
|
||||||
|
|
@ -14,13 +14,13 @@
|
||||||
"Interface": 3,
|
"Interface": 3,
|
||||||
"Method": 1,
|
"Method": 1,
|
||||||
"Process": 4,
|
"Process": 4,
|
||||||
"Property": 8
|
"Property": 18
|
||||||
},
|
},
|
||||||
"byRelType": {
|
"byRelType": {
|
||||||
"ACCESSES": 3,
|
"ACCESSES": 3,
|
||||||
"CALLS": 9,
|
"CALLS": 9,
|
||||||
"CONTAINS": 7,
|
"CONTAINS": 7,
|
||||||
"DEFINES": 16,
|
"DEFINES": 26,
|
||||||
"HAS_METHOD": 1,
|
"HAS_METHOD": 1,
|
||||||
"HAS_PROPERTY": 8,
|
"HAS_PROPERTY": 8,
|
||||||
"IMPORTS": 12,
|
"IMPORTS": 12,
|
||||||
|
|
@ -28,5 +28,5 @@
|
||||||
"STEP_IN_PROCESS": 12,
|
"STEP_IN_PROCESS": 12,
|
||||||
"USES": 5
|
"USES": 5
|
||||||
},
|
},
|
||||||
"edgeDigest": "c55bd8307a5fccbfd23d5aa93f241e26ef0f45c1a9e57691f2bf4f6602e8e4ab"
|
"edgeDigest": "5dddd1f466deeda1197eb61b480a4f3a5da67dfc0d00ace22377158366e87d00"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,11 +145,21 @@ describe('JavaScript plain-object property access (A1/A5)', () => {
|
||||||
expect(writersOf('destructuredOnlyField')).toContain('buildFlat');
|
expect(writersOf('destructuredOnlyField')).toContain('buildFlat');
|
||||||
});
|
});
|
||||||
|
|
||||||
// The point of modelling these as writes rather than definitions: more
|
// This asserted `toHaveLength(1)` — no new definition — until R3-4 began
|
||||||
// definitions would add same-named competitors to the narrowing that makes
|
// anchoring returned literals, which mints exactly one here (`buildFlat`'s
|
||||||
// these fields resolvable in the first place.
|
// return shape). The assertion was the right instinct expressed as the
|
||||||
it('mints NO new definition for a constructed record', () => {
|
// wrong invariant: what R2-1b actually protects is that adding definitions
|
||||||
expect(propertyNames().filter((n) => n === 'destructuredOnlyField')).toHaveLength(1);
|
// 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', () => {
|
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
|
// R2. Strict workspace uniqueness was measurably too blunt: in the reporting
|
||||||
// repo `exitMinAtrMult` had 26 definitions, 16 of them in one-off scripts the
|
// 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
|
// 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', () => {
|
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.
|
// 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
|
// 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 48 so concurrent bumps cannot silently collide (#2766)', () => {
|
it('pins SCHEMA_BUMP to 49 so concurrent bumps cannot silently collide (#2766)', () => {
|
||||||
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(48);
|
expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(49);
|
||||||
});
|
});
|
||||||
|
|
||||||
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