Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/extractors/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2043,6 +2043,28 @@ function extractParamBindingsWalk(rootNode: TreeSitterNode, paramBindings: Param
if (ct === ',' || ct === '(' || ct === ')') continue;
if (ct === 'identifier' && !BUILTIN_GLOBALS.has(child.text)) {
paramBindings.push({ callee: fn.text, argIndex: argIdx, argName: child.text });
} else if (ct === 'spread_element') {
// f(...[a, b]) — inline array literal: expand each element as a direct param binding.
const inner =
child.childForFieldName('argument') ?? (child.childCount > 1 ? child.child(1) : null);
if (inner?.type === 'array') {
let elemCount = 0;
for (let j = 0; j < inner.childCount; j++) {
const elem = inner.child(j);
if (!elem) continue;
if (elem.type === ',' || elem.type === '[' || elem.type === ']') continue;
if (elem.type === 'identifier' && !BUILTIN_GLOBALS.has(elem.text)) {
paramBindings.push({
callee: fn.text,
argIndex: argIdx + elemCount,
argName: elem.text,
});
}
elemCount++;
}
// argIdx++ below accounts for 1 slot; pre-advance for the remaining elements.
argIdx += Math.max(0, elemCount - 1);
}
Comment on lines +2150 to +2170

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Empty inline-array spread incorrectly advances argIdx by 1. When ...[ ] has zero elements, elemCount is 0, so argIdx += Math.max(0, -1) = 0, then the unconditional argIdx++ below still fires. Any positional argument appearing after ...[ ] in the same call receives an index one too high, causing a missed param binding. Replacing the pre-advance + unconditional increment with a single argIdx += elemCount; continue fully controls the slot consumption inside the branch and also simplifies the arithmetic.

Suggested change
if (inner?.type === 'array') {
let elemCount = 0;
for (let j = 0; j < inner.childCount; j++) {
const elem = inner.child(j);
if (!elem) continue;
if (elem.type === ',' || elem.type === '[' || elem.type === ']') continue;
if (elem.type === 'identifier' && !BUILTIN_GLOBALS.has(elem.text)) {
paramBindings.push({
callee: fn.text,
argIndex: argIdx + elemCount,
argName: elem.text,
});
}
elemCount++;
}
// argIdx++ below accounts for 1 slot; pre-advance for the remaining elements.
argIdx += Math.max(0, elemCount - 1);
}
if (inner?.type === 'array') {
let elemCount = 0;
for (let j = 0; j < inner.childCount; j++) {
const elem = inner.child(j);
if (!elem) continue;
if (elem.type === ',' || elem.type === '[' || elem.type === ']') continue;
if (elem.type === 'identifier' && !BUILTIN_GLOBALS.has(elem.text)) {
paramBindings.push({
callee: fn.text,
argIndex: argIdx + elemCount,
argName: elem.text,
});
}
elemCount++;
}
// Advance by the exact number of slots this spread occupies and skip
// the unconditional argIdx++ below so that zero-element spreads (...[])
// do not shift subsequent argument indices.
argIdx += elemCount;
continue;
}

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The empty inline-array spread bug is real — replaced argIdx += Math.max(0, elemCount - 1) plus the unconditional argIdx++ with argIdx += elemCount; continue, so zero-element spreads consume zero slots and do not shift subsequent argument indices. Added a test case r(...[], e) to lock in the fix: e must resolve to index 0 (not 1) in r, confirming the corrected behavior.

}
argIdx++;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "../../expected-edges.schema.json",
"language": "javascript",
"description": "Spread operator call resolution — named functions spread as arguments",
"description": "Spread operator call resolution — named functions spread as arguments (variable and inline array)",
"edges": [
{
"source": { "name": "f", "file": "spread.js" },
Expand All @@ -26,6 +26,48 @@
"target": { "name": "d", "file": "spread.js" },
"kind": "calls",
"mode": "pts-spread"
},
{
"source": { "name": "p", "file": "spread.js" },
"target": { "name": "e", "file": "spread.js" },
"kind": "calls",
"mode": "pts-param"
},
{
"source": { "name": "q", "file": "spread.js" },
"target": { "name": "a", "file": "spread.js" },
"kind": "calls",
"mode": "pts-param"
},
{
"source": { "name": "q", "file": "spread.js" },
"target": { "name": "b", "file": "spread.js" },
"kind": "calls",
"mode": "pts-param"
},
{
"source": { "name": "q", "file": "spread.js" },
"target": { "name": "c", "file": "spread.js" },
"kind": "calls",
"mode": "pts-param"
},
{
"source": { "name": "q", "file": "spread.js" },
"target": { "name": "e", "file": "spread.js" },
"kind": "calls",
"mode": "pts-param"
},
{
"source": { "name": "q", "file": "spread.js" },
"target": { "name": "h", "file": "spread.js" },
"kind": "calls",
"mode": "pts-param"
},
{
"source": { "name": "q", "file": "spread.js" },
"target": { "name": "d", "file": "spread.js" },
"kind": "calls",
"mode": "pts-param"
}
]
}
14 changes: 14 additions & 0 deletions tests/benchmarks/resolution/fixtures/jelly-micro/spread/spread.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ function a() {}
function b() {}
function c() {}
function d() {}
function e() {}
function h() {}

function f(x, y) {
x();
Expand All @@ -13,9 +15,21 @@ function g(x, y) {
x();
y();
}
function p(x) {
x();
}
function q(x, y, z) {
x();
y();
z();
}

const arr1 = [a, b];
f(...arr1); // f→a, f→b

const arr2 = [c, d];
g(...arr2); // g→c, g→d

p(...[e]); // p→e (inline single-element array)
q(...[a, b, c]); // q→a, q→b, q→c (inline multi-element array)
q(e, ...[h, d]); // q→e (pos 0), q→h (pos 1), q→d (pos 2) (mixed: identifier + inline spread)
Loading