Skip to content

Commit 7019424

Browse files
authored
fix(extractor): narrow .call/.apply/.bind skip in extractCallbackReferenceCalls (#1420)
* fix(extractor): narrow .call/.apply/.bind skip in extractCallbackReferenceCalls For .call()/.apply(): the first argument is the this-context and is now skipped; subsequent identifier arguments are emitted as genuine callbacks. e.g. forEach.call(arr, handler) → emits handler, not arr. For .bind(): all arguments are absorbed into the partially-applied function and none are direct callbacks — return [] immediately. Closes #1406 * 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. * test(extractor): align .call()-with-only-this-context assertion style (#1420)
1 parent 9037c13 commit 7019424

2 files changed

Lines changed: 32 additions & 0 deletions

File tree

src/extractors/javascript.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2905,6 +2905,11 @@ function firstArgIsStringLiteral(argsNode: TreeSitterNode): boolean {
29052905
* member-expr args are only emitted when the first argument is a string
29062906
* literal route path — matching Express/router shape and skipping
29072907
* `cache.get(user.id)`-style calls.
2908+
*
2909+
* `.call()` / `.apply()` / `.bind()` — the first arg is the `this` context (not a callback of
2910+
* the enclosing function) and subsequent args flow into the delegated function's parameters.
2911+
* Emitting them here would produce false-positive edges from the *calling* function.
2912+
* This-rebinding (fn::this → ctx) is handled separately by extractThisCallBindingsWalk.
29082913
*/
29092914
function extractCallbackReferenceCalls(callNode: TreeSitterNode): Call[] {
29102915
const args = callNode.childForFieldName('arguments') || findChild(callNode, 'arguments');
@@ -2916,6 +2921,7 @@ function extractCallbackReferenceCalls(callNode: TreeSitterNode): Call[] {
29162921
// Emitting them here would produce false-positive edges from the *calling* function.
29172922
// This-rebinding (fn::this → ctx) is handled separately by extractThisCallBindingsWalk.
29182923
if (calleeName === 'call' || calleeName === 'apply' || calleeName === 'bind') return [];
2924+
29192925
let memberExprArgsAllowed = calleeName !== null && CALLBACK_ACCEPTING_CALLEES.has(calleeName);
29202926
if (memberExprArgsAllowed && calleeName !== null && HTTP_VERB_CALLEES.has(calleeName)) {
29212927
// HTTP verbs require a string-literal route path to be treated as a

tests/parsers/javascript.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,32 @@ describe('JavaScript parser', () => {
850850
expect(def.line).toBe(2);
851851
expect(def.endLine).toBe(4);
852852
});
853+
854+
// .call/.apply/.bind narrowing (#1406)
855+
// All args flow into the delegated function, not as callbacks for the current scope.
856+
// This-rebinding (fn::this → ctx) is handled by extractThisCallBindingsWalk instead.
857+
it('emits nothing for .call() — args flow into the delegated function, not the current scope', () => {
858+
const symbols = parseJS(`Array.prototype.forEach.call(collection, handler);`);
859+
expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'handler' }));
860+
expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'collection' }));
861+
});
862+
863+
it('emits nothing for .apply() — second arg is an arguments array, not a callback', () => {
864+
const symbols = parseJS(`fn.apply(ctx, handler);`);
865+
expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'handler' }));
866+
expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'ctx' }));
867+
});
868+
869+
it('emits nothing for .call() with only the this-context arg', () => {
870+
const symbols = parseJS(`fn.call(ctx);`);
871+
expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'ctx' }));
872+
});
873+
874+
it('emits nothing for .bind() — all args are absorbed into the partial application', () => {
875+
const symbols = parseJS(`Promise.resolve.bind(null, transform);`);
876+
expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'transform' }));
877+
expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'null' }));
878+
});
853879
});
854880

855881
describe('Phase 8.3f: object-destructuring rest parameter binding extraction', () => {

0 commit comments

Comments
 (0)