fix(extractor): narrow .call/.apply/.bind skip in extractCallbackReferenceCalls#1420
Conversation
…renceCalls 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
Greptile SummaryThis PR documents and tests the
Confidence Score: 5/5Safe to merge — the change is documentation and tests only; the behaviour-changing guard was already committed. The only lines changed in the extractor are a JSDoc block and a blank line. The four new tests are well-scoped negative assertions that correctly reflect the intended semantics for all three methods. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[extractCallbackReferenceCalls] --> B{calleeName?}
B -->|'call' / 'apply' / 'bind'| C["return [] — no edges emitted\n(args belong to delegated fn scope)"]
B -->|other callee| D{Is calleeName in CALLBACK_ACCEPTING_CALLEES?}
D -->|yes| E{Is calleeName in HTTP_VERB_CALLEES?}
E -->|yes| F{First arg is string literal?}
F -->|yes| G[memberExprArgsAllowed = true]
F -->|no| H[memberExprArgsAllowed = false]
E -->|no| G
D -->|no| H
G --> I[Emit identifier args + member-expr args]
H --> J[Emit identifier args only]
C -.->|this-rebinding handled separately| K[extractThisCallBindingsWalk]
Reviews (11): Last reviewed commit: "Merge branch 'main' into fix/narrow-call..." | Re-trigger Greptile |
| // .bind() absorbs all arguments into a partially-applied function — no direct callbacks. | ||
| if (calleeName === 'bind') return []; | ||
|
|
||
| const skipFirstArg = calleeName === 'call' || calleeName === 'apply'; |
There was a problem hiding this comment.
.apply() second argument incorrectly emitted as a callback. Unlike .call(thisArg, arg1, arg2, …), .apply() takes exactly two arguments: thisArg and a single argsArray. The array argument is never a direct callback — it is the arguments to spread into the target function. Emitting an identifier second arg (e.g. Math.max.apply(null, numbers)) will produce a false-positive dynamic call edge to numbers, exactly the kind of noise this PR is intended to eliminate. .apply() should be treated the same as .bind() and bail out immediately.
| // .bind() absorbs all arguments into a partially-applied function — no direct callbacks. | |
| if (calleeName === 'bind') return []; | |
| const skipFirstArg = calleeName === 'call' || calleeName === 'apply'; | |
| // .bind() and .apply() absorb all arguments — no direct callbacks. | |
| // .apply(thisArg, argsArray): argsArray is a single array, never a callback. | |
| if (calleeName === 'bind' || calleeName === 'apply') return []; | |
| const skipFirstArg = calleeName === 'call'; |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Fixed — .apply() now returns [] immediately alongside .bind(). The JSDoc comment above the function has also been updated to explain that .apply(thisArg, argsArray) never carries a direct callback. Committed in 3ab27b6.
| it('emits identifier args after the this-context for .apply()', () => { | ||
| const symbols = parseJS(`fn.apply(ctx, handler);`); | ||
| expect(symbols.calls).toContainEqual( | ||
| expect.objectContaining({ name: 'handler', dynamic: true }), | ||
| ); | ||
| expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'ctx' })); | ||
| }); |
There was a problem hiding this comment.
Test validates the false-positive
.apply() behavior. fn.apply(ctx, handler) passes handler as the arguments array, not as a callback. The test asserting that handler is emitted confirms incorrect behavior — if the extractor fix above is applied, this test should instead assert nothing is emitted (mirroring the .bind() test).
| it('emits identifier args after the this-context for .apply()', () => { | |
| const symbols = parseJS(`fn.apply(ctx, handler);`); | |
| expect(symbols.calls).toContainEqual( | |
| expect.objectContaining({ name: 'handler', dynamic: true }), | |
| ); | |
| expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'ctx' })); | |
| }); | |
| it('emits nothing for .apply() — second arg is an arguments array, not a callback', () => { | |
| const symbols = parseJS(`fn.apply(ctx, handler);`); | |
| expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'handler' })); | |
| expect(symbols.calls).not.toContainEqual(expect.objectContaining({ name: 'ctx' })); | |
| }); |
There was a problem hiding this comment.
Fixed — the test now asserts that neither handler nor ctx is emitted for .apply(), mirroring the .bind() test. Committed in 3ab27b6.
Codegraph Impact Analysis1 functions changed → 6 callers affected across 1 files
|
…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.
|
Addressed the PR description mismatch flagged in the Greptile summary: the description previously claimed |
Summary
.call()/.apply()/.bind(): all arguments are absorbed into the delegated function's parameter scope — none are direct callbacks of the calling function. This PR skips all arguments immediately for all three, eliminating false-positive dynamic call edges..call(thisArg, arg1, …)and.apply(thisArg, argsArray): the first arg is thethiscontext; remaining args flow into the delegated function, not the current scope..bind(thisArg, arg1, …): all args are absorbed into the partial application.fn::this → ctx) continues to be handled separately byextractThisCallBindingsWalk.Previously, identifier arguments were unconditionally emitted for
.call()/.apply()calls, causingthis-context arguments (and subsequent positional args) to be treated as dynamic callbacks. This produced false-positive call edges.Test plan
tests/parsers/javascript.test.tscover:.call()emits nothing;.apply()emits nothing;.call()with only this-context emits nothing;.bind()emits nothingCloses #1406