Skip to content

Commit 3ab27b6

Browse files
committed
fix(extractor): treat .apply() like .bind() — its argsArray is never a callback
Unlike .call(thisArg, cb1, cb2…), .apply() takes exactly two args: thisArg and a single argsArray. The array is never a direct callback, so emitting its identifier (e.g. Math.max.apply(null, numbers) → edge to numbers) is a false-positive. Return [] immediately for .apply(), same as .bind(). Update the unit test to assert no edges are emitted.
1 parent 519173f commit 3ab27b6

2 files changed

Lines changed: 14 additions & 12 deletions

File tree

src/extractors/javascript.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2727,22 +2727,26 @@ function firstArgIsStringLiteral(argsNode: TreeSitterNode): boolean {
27272727
* literal route path — matching Express/router shape and skipping
27282728
* `cache.get(user.id)`-style calls.
27292729
*
2730-
* `.call()`/`.apply()`: the first argument is the `this` context (not a
2731-
* callback). Subsequent identifier arguments are genuine callbacks of the
2732-
* enclosing scope — e.g. `Array.prototype.forEach.call(arr, handler)` emits
2733-
* `handler`. `.bind()` returns a new partially-applied function; all
2734-
* arguments are absorbed and none are direct callbacks.
2730+
* `.call()`: the first argument is the `this` context (not a callback).
2731+
* Subsequent identifier arguments are genuine callbacks of the enclosing
2732+
* scope — e.g. `Array.prototype.forEach.call(arr, handler)` emits `handler`.
2733+
* `.bind()` returns a new partially-applied function; all arguments are
2734+
* absorbed and none are direct callbacks.
2735+
* `.apply()` takes exactly two arguments: `thisArg` and a single `argsArray`.
2736+
* The array is never a direct callback, so `.apply()` is treated like `.bind()`
2737+
* and returns immediately with no edges.
27352738
*/
27362739
function extractCallbackReferenceCalls(callNode: TreeSitterNode): Call[] {
27372740
const args = callNode.childForFieldName('arguments') || findChild(callNode, 'arguments');
27382741
if (!args) return [];
27392742

27402743
const calleeName = extractCalleeName(callNode);
27412744

2742-
// .bind() absorbs all arguments into a partially-applied function — no direct callbacks.
2743-
if (calleeName === 'bind') return [];
2745+
// .bind() and .apply() absorb all arguments — no direct callbacks.
2746+
// .apply(thisArg, argsArray): argsArray is a single array, never a callback.
2747+
if (calleeName === 'bind' || calleeName === 'apply') return [];
27442748

2745-
const skipFirstArg = calleeName === 'call' || calleeName === 'apply';
2749+
const skipFirstArg = calleeName === 'call';
27462750

27472751
let memberExprArgsAllowed = calleeName !== null && CALLBACK_ACCEPTING_CALLEES.has(calleeName);
27482752
if (memberExprArgsAllowed && calleeName !== null && HTTP_VERB_CALLEES.has(calleeName)) {

tests/parsers/javascript.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -810,11 +810,9 @@ describe('JavaScript parser', () => {
810810
expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'collection' }));
811811
});
812812

813-
it('emits identifier args after the this-context for .apply()', () => {
813+
it('emits nothing for .apply() — second arg is an arguments array, not a callback', () => {
814814
const symbols = parseJS(`fn.apply(ctx, handler);`);
815-
expect(symbols.calls).toContainEqual(
816-
expect.objectContaining({ name: 'handler', dynamic: true }),
817-
);
815+
expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'handler' }));
818816
expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'ctx' }));
819817
});
820818

0 commit comments

Comments
 (0)