Skip to content

Commit c484fd1

Browse files
committed
feat(resolver): resolve property calls on object destructuring rest parameters (JS)
Adds Phase 8.3f: when a function parameter uses object destructuring with a rest element (`function f3({ e1: eee1, ...eerest })`), and the rest object's property is called (`eerest.e4()`), resolve the callee via a three-hop chain: ObjectRestParamBinding (eerest ← f3 param 0) + ParamBinding (f3(obj) → obj at index 0) + ObjectPropBinding (obj = { e4 } → obj.e4 = e4) → pts["eerest.e4"] = {"e4"} → calls edge f3 → e4 Changes: - types.ts: add ObjectRestParamBinding and ObjectPropBinding interfaces - javascript.ts: extractObjectRestParamBindingsWalk (finds rest params in object-destructured function params) and extractObjectPropBindingsWalk (finds shorthand/identifier properties in object literals); wired into both extractSymbolsQuery and extractSymbolsWalk paths - wasm-worker-{protocol,entry,pool}.ts: serialize new binding arrays - points-to.ts: seed pts["rest.propName"] = {"fn"} from the three-hop chain - build-edges.ts: new Phase 8.3f receiver-pts fallback — when a receiver call is unresolved, check pts["receiver.name"] for rest-dispatch targets; also include new bindings in buildPointsToMapForFile null-check guard Jelly micro-test benchmark (rest fixture): recall=100% TP=1 FN=0 FP=0 Closes #1336
1 parent a6c5d2d commit c484fd1

9 files changed

Lines changed: 240 additions & 1 deletion

File tree

src/domain/graph/builder/stages/build-edges.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,9 @@ function buildPointsToMapForFile(
836836
!symbols.arrayElemBindings?.length &&
837837
!symbols.spreadArgBindings?.length &&
838838
!symbols.forOfBindings?.length &&
839-
!symbols.arrayCallbackBindings?.length
839+
!symbols.arrayCallbackBindings?.length &&
840+
!symbols.objectRestParamBindings?.length &&
841+
!symbols.objectPropBindings?.length
840842
)
841843
return null;
842844
const defNames = new Set(
@@ -855,6 +857,8 @@ function buildPointsToMapForFile(
855857
symbols.spreadArgBindings,
856858
symbols.forOfBindings,
857859
symbols.arrayCallbackBindings,
860+
symbols.objectRestParamBindings,
861+
symbols.objectPropBindings,
858862
);
859863
}
860864

@@ -1007,6 +1011,43 @@ function buildFileCallEdges(
10071011
}
10081012
}
10091013

1014+
// Phase 8.3f: pts fallback for receiver calls via object-rest param bindings.
1015+
// Fires when `rest.prop()` is encountered and `rest` was seeded as `pts["rest.prop"]`
1016+
// by the object-rest dispatch chain (ObjectRestParamBinding + paramBinding + ObjectPropBinding).
1017+
if (
1018+
targets.length === 0 &&
1019+
call.receiver &&
1020+
!BUILTIN_RECEIVERS.has(call.receiver) &&
1021+
call.receiver !== 'this' &&
1022+
call.receiver !== 'self' &&
1023+
call.receiver !== 'super' &&
1024+
ptsMap
1025+
) {
1026+
const receiverKey = `${call.receiver}.${call.name}`;
1027+
if (ptsMap.has(receiverKey)) {
1028+
for (const alias of resolveViaPointsTo(receiverKey, ptsMap)) {
1029+
const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(
1030+
lookup,
1031+
{ name: alias },
1032+
relPath,
1033+
importedNames,
1034+
typeMap as Map<string, unknown>,
1035+
);
1036+
for (const t of aliasTargets) {
1037+
const edgeKey = `${caller.id}|${t.id}`;
1038+
if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
1039+
const conf =
1040+
computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
1041+
if (conf > 0) {
1042+
ptsEdgeRows.set(edgeKey, allEdgeRows.length);
1043+
allEdgeRows.push([caller.id, t.id, 'calls', conf, isDynamic, 'points-to']);
1044+
}
1045+
}
1046+
}
1047+
}
1048+
}
1049+
}
1050+
10101051
if (
10111052
call.receiver &&
10121053
!BUILTIN_RECEIVERS.has(call.receiver) &&

src/domain/graph/resolver/points-to.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import type {
2424
ArrayElemBinding,
2525
FnRefBinding,
2626
ForOfBinding,
27+
ObjectPropBinding,
28+
ObjectRestParamBinding,
2729
ParamBinding,
2830
SpreadArgBinding,
2931
} from '../../../types.js';
@@ -68,6 +70,8 @@ export function buildPointsToMap(
6870
spreadArgBindings?: readonly SpreadArgBinding[],
6971
forOfBindings?: readonly ForOfBinding[],
7072
arrayCallbackBindings?: readonly ArrayCallbackBinding[],
73+
objectRestParamBindings?: readonly ObjectRestParamBinding[],
74+
objectPropBindings?: readonly ObjectPropBinding[],
7175
): PointsToMap {
7276
const pts: PointsToMap = new Map();
7377

@@ -177,6 +181,24 @@ export function buildPointsToMap(
177181
}
178182
}
179183

184+
// Phase 8.3f: object-rest parameter dispatch.
185+
// `function f({ ...rest }) {}` + `f(obj)` + `const obj = { prop: fn }` →
186+
// seed pts["rest.prop"] = {"fn"} so that `rest.prop()` resolves to `fn`.
187+
if (objectRestParamBindings && objectPropBindings && paramBindings) {
188+
for (const { callee, restName, argIndex } of objectRestParamBindings) {
189+
for (const { callee: pbCallee, argIndex: pbArgIdx, argName } of paramBindings) {
190+
if (pbCallee !== callee || pbArgIdx !== argIndex) continue;
191+
for (const { objectName, propName, valueName } of objectPropBindings) {
192+
if (objectName !== argName) continue;
193+
if (!definitionNames.has(valueName) && !importedNames.has(valueName)) continue;
194+
const key = `${restName}.${propName}`;
195+
if (!pts.has(key)) pts.set(key, new Set());
196+
pts.get(key)!.add(valueName);
197+
}
198+
}
199+
}
200+
}
201+
180202
if (constraints.length === 0) return pts;
181203

182204
// Fixed-point iteration: propagate pts sets until no new information flows.

src/domain/wasm-worker-entry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,10 @@ function serializeExtractorOutput(
810810
...(symbols.arrayCallbackBindings?.length
811811
? { arrayCallbackBindings: symbols.arrayCallbackBindings }
812812
: {}),
813+
...(symbols.objectRestParamBindings?.length
814+
? { objectRestParamBindings: symbols.objectRestParamBindings }
815+
: {}),
816+
...(symbols.objectPropBindings?.length ? { objectPropBindings: symbols.objectPropBindings } : {}),
813817
...(symbols.newExpressions?.length ? { newExpressions: symbols.newExpressions } : {}),
814818
};
815819
}

src/domain/wasm-worker-pool.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ function deserializeResult(ser: SerializedExtractorOutput | null): ExtractorOutp
112112
if (ser.spreadArgBindings?.length) out.spreadArgBindings = ser.spreadArgBindings;
113113
if (ser.forOfBindings?.length) out.forOfBindings = ser.forOfBindings;
114114
if (ser.arrayCallbackBindings?.length) out.arrayCallbackBindings = ser.arrayCallbackBindings;
115+
if (ser.objectRestParamBindings?.length) out.objectRestParamBindings = ser.objectRestParamBindings;
116+
if (ser.objectPropBindings?.length) out.objectPropBindings = ser.objectPropBindings;
115117
if (ser.newExpressions?.length) out.newExpressions = ser.newExpressions;
116118
return out;
117119
}

src/domain/wasm-worker-protocol.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ export interface SerializedExtractorOutput {
6868
spreadArgBindings?: import('../types.js').SpreadArgBinding[];
6969
forOfBindings?: import('../types.js').ForOfBinding[];
7070
arrayCallbackBindings?: import('../types.js').ArrayCallbackBinding[];
71+
objectRestParamBindings?: import('../types.js').ObjectRestParamBinding[];
72+
objectPropBindings?: import('../types.js').ObjectPropBinding[];
7173
newExpressions?: readonly string[];
7274
}
7375

src/extractors/javascript.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {
1111
FnRefBinding,
1212
ForOfBinding,
1313
Import,
14+
ObjectPropBinding,
15+
ObjectRestParamBinding,
1416
ParamBinding,
1517
SpreadArgBinding,
1618
SubDeclaration,
@@ -332,6 +334,8 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
332334
const spreadArgBindings: SpreadArgBinding[] = [];
333335
const forOfBindings: ForOfBinding[] = [];
334336
const arrayCallbackBindings: ArrayCallbackBinding[] = [];
337+
const objectRestParamBindings: ObjectRestParamBinding[] = [];
338+
const objectPropBindings: ObjectPropBinding[] = [];
335339

336340
const matches = query.matches(tree.rootNode);
337341

@@ -373,6 +377,10 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
373377
// Extract definitions from destructured bindings (query patterns don't match object_pattern)
374378
extractDestructuredBindingsWalk(tree.rootNode, definitions);
375379

380+
// Phase 8.3f: Extract object-rest parameter and object-property bindings
381+
extractObjectRestParamBindingsWalk(tree.rootNode, objectRestParamBindings);
382+
extractObjectPropBindingsWalk(tree.rootNode, objectPropBindings);
383+
376384
// Phase 8.5: collect all `new X()` constructor names for RTA instantiation tracking
377385
const newExpressions: string[] = [];
378386
extractNewExpressionsWalk(tree.rootNode, newExpressions);
@@ -392,6 +400,8 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
392400
spreadArgBindings,
393401
forOfBindings,
394402
arrayCallbackBindings,
403+
objectRestParamBindings,
404+
objectPropBindings,
395405
newExpressions,
396406
};
397407
}
@@ -629,6 +639,8 @@ function extractSymbolsWalk(tree: TreeSitterTree): ExtractorOutput {
629639
spreadArgBindings: [],
630640
forOfBindings: [],
631641
arrayCallbackBindings: [],
642+
objectRestParamBindings: [],
643+
objectPropBindings: [],
632644
};
633645

634646
walkJavaScriptNode(tree.rootNode, ctx);
@@ -657,6 +669,9 @@ function extractSymbolsWalk(tree: TreeSitterTree): ExtractorOutput {
657669
ctx.arrayCallbackBindings!,
658670
ctx.fnRefBindings!,
659671
);
672+
// Phase 8.3f: Extract object-rest parameter and object-property bindings
673+
extractObjectRestParamBindingsWalk(tree.rootNode, ctx.objectRestParamBindings!);
674+
extractObjectPropBindingsWalk(tree.rootNode, ctx.objectPropBindings!);
660675
// Phase 8.5: collect all `new X()` constructor names for RTA instantiation tracking
661676
const newExpressions: string[] = [];
662677
extractNewExpressionsWalk(tree.rootNode, newExpressions);
@@ -1871,6 +1886,101 @@ function extractSpreadForOfWalk(
18711886
walk(rootNode, 0);
18721887
}
18731888

1889+
/**
1890+
* Phase 8.3f: collect object-rest parameter bindings.
1891+
*
1892+
* `function f({ a, ...rest }) {}` → `{ callee: "f", restName: "rest", argIndex: 0 }`
1893+
*
1894+
* Enables resolving `rest.prop()` when a known object is passed as that parameter.
1895+
*/
1896+
function extractObjectRestParamBindingsWalk(
1897+
rootNode: TreeSitterNode,
1898+
bindings: ObjectRestParamBinding[],
1899+
): void {
1900+
function walk(node: TreeSitterNode, depth: number): void {
1901+
if (depth >= MAX_WALK_DEPTH) return;
1902+
const t = node.type;
1903+
if (t === 'function_declaration' || t === 'function_expression' || t === 'arrow_function') {
1904+
const nameNode = node.childForFieldName('name');
1905+
const funcName = nameNode?.text;
1906+
if (funcName) {
1907+
const paramsNode =
1908+
node.childForFieldName('parameters') || findChild(node, 'formal_parameters');
1909+
if (paramsNode) {
1910+
let argIndex = 0;
1911+
for (let i = 0; i < paramsNode.childCount; i++) {
1912+
const param = paramsNode.child(i);
1913+
if (!param) continue;
1914+
const pt = param.type;
1915+
if (pt === ',' || pt === '(' || pt === ')') continue;
1916+
if (pt === 'object_pattern') {
1917+
for (let j = 0; j < param.childCount; j++) {
1918+
const child = param.child(j);
1919+
if (!child) continue;
1920+
if (child.type !== 'rest_pattern' && child.type !== 'rest_element') continue;
1921+
const restNameNode = child.child(1) ?? child.childForFieldName('name');
1922+
if (restNameNode?.type === 'identifier') {
1923+
bindings.push({ callee: funcName, restName: restNameNode.text, argIndex });
1924+
}
1925+
}
1926+
}
1927+
argIndex++;
1928+
}
1929+
}
1930+
}
1931+
}
1932+
for (let i = 0; i < node.childCount; i++) {
1933+
walk(node.child(i)!, depth + 1);
1934+
}
1935+
}
1936+
walk(rootNode, 0);
1937+
}
1938+
1939+
/**
1940+
* Phase 8.3f: collect object-property bindings from object literals.
1941+
*
1942+
* `const obj = { e4 }` → `{ objectName: "obj", propName: "e4", valueName: "e4" }`
1943+
* `const obj = { e1: fn }` → `{ objectName: "obj", propName: "e1", valueName: "fn" }`
1944+
*
1945+
* Only tracks shorthand and `key: identifier` pairs; skips function literals.
1946+
*/
1947+
function extractObjectPropBindingsWalk(
1948+
rootNode: TreeSitterNode,
1949+
bindings: ObjectPropBinding[],
1950+
): void {
1951+
function walk(node: TreeSitterNode, depth: number): void {
1952+
if (depth >= MAX_WALK_DEPTH) return;
1953+
if (node.type === 'variable_declarator') {
1954+
const nameN = node.childForFieldName('name');
1955+
const valueN = node.childForFieldName('value');
1956+
if (nameN?.type === 'identifier' && valueN?.type === 'object') {
1957+
const objectName = nameN.text;
1958+
for (let i = 0; i < valueN.childCount; i++) {
1959+
const child = valueN.child(i);
1960+
if (!child) continue;
1961+
if (child.type === 'shorthand_property_identifier') {
1962+
bindings.push({ objectName, propName: child.text, valueName: child.text });
1963+
} else if (child.type === 'pair') {
1964+
const keyN = child.childForFieldName('key');
1965+
const valN = child.childForFieldName('value');
1966+
if (
1967+
keyN?.type === 'property_identifier' &&
1968+
valN?.type === 'identifier' &&
1969+
!BUILTIN_GLOBALS.has(valN.text)
1970+
) {
1971+
bindings.push({ objectName, propName: keyN.text, valueName: valN.text });
1972+
}
1973+
}
1974+
}
1975+
}
1976+
}
1977+
for (let i = 0; i < node.childCount; i++) {
1978+
walk(node.child(i)!, depth + 1);
1979+
}
1980+
}
1981+
walk(rootNode, 0);
1982+
}
1983+
18741984
function extractReceiverName(objNode: TreeSitterNode | null): string | undefined {
18751985
if (!objNode) return undefined;
18761986
const t = objNode.type;

src/types.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,33 @@ export interface ArrayCallbackBinding {
603603
calleeName: string;
604604
}
605605

606+
/**
607+
* An object-rest parameter binding: `function f({ a, ...rest })` records that
608+
* `rest` is the rest of the object passed as argument `argIndex` to `f`.
609+
* Phase 8.3f: object destructuring rest dispatch.
610+
*/
611+
export interface ObjectRestParamBinding {
612+
/** Function that owns this rest parameter, e.g. "f3" */
613+
callee: string;
614+
/** Name of the rest binding, e.g. "eerest" */
615+
restName: string;
616+
/** Zero-based index of the argument whose rest is bound, e.g. 0 */
617+
argIndex: number;
618+
}
619+
620+
/**
621+
* An object-property binding: `const obj = { e4 }` or `const obj = { e4: fn }` records
622+
* that `obj.e4` points to the named function `fn`. Phase 8.3f.
623+
*/
624+
export interface ObjectPropBinding {
625+
/** Variable holding the object, e.g. "obj" */
626+
objectName: string;
627+
/** Property name, e.g. "e4" */
628+
propName: string;
629+
/** Named function value, e.g. "e4" or "fn" */
630+
valueName: string;
631+
}
632+
606633
/** The normalized output shape returned by every language extractor. */
607634
export interface ExtractorOutput {
608635
definitions: Definition[];
@@ -642,6 +669,10 @@ export interface ExtractorOutput {
642669
forOfBindings?: ForOfBinding[];
643670
/** Phase 8.3e: array callback bindings from Array.from/forEach/etc. */
644671
arrayCallbackBindings?: ArrayCallbackBinding[];
672+
/** Phase 8.3f: object-rest parameter bindings from `function f({ ...rest })` patterns. */
673+
objectRestParamBindings?: ObjectRestParamBinding[];
674+
/** Phase 8.3f: object-property bindings from `const obj = { fn }` patterns. */
675+
objectPropBindings?: ObjectPropBinding[];
645676
/**
646677
* Phase 8.5 (RTA): constructor names from all `new X()` expressions in the file,
647678
* including unassigned ones (e.g. `doSomething(new Foo())`). Used to build the
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "../../expected-edges.schema.json",
3+
"language": "javascript",
4+
"description": "Object destructuring rest parameter dispatch — eerest.e4() → e4",
5+
"edges": [
6+
{
7+
"source": { "name": "f3", "file": "rest.js" },
8+
"target": { "name": "e4", "file": "rest.js" },
9+
"kind": "calls",
10+
"mode": "pts-obj-rest"
11+
}
12+
]
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Jelly micro-test: rest — object destructuring rest parameter dispatch
2+
3+
function e1() {}
4+
function e2() {}
5+
function e3() {}
6+
function e4() {}
7+
8+
const obj = { e1, e2, e3, e4 };
9+
10+
function f3({ e1: eee1, ...eerest }) {
11+
eee1();
12+
eerest.e4(); // eerest.e4 === obj.e4 === e4 when called as f3(obj)
13+
}
14+
f3(obj);

0 commit comments

Comments
 (0)