Skip to content

Commit 4d1cd53

Browse files
authored
Merge branch 'main' into fix/static-receiver-confidence-1398
2 parents b524a8e + 3b29844 commit 4d1cd53

4 files changed

Lines changed: 34 additions & 2 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/benchmarks/resolution/resolution-benchmark.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ const THRESHOLDS: Record<string, { precision: number; recall: number }> = {
129129
// total expected now 35. multi-class.js adds 4 class-scoped typeMap edges (#1382) → 39.
130130
// call/apply this-rebinding adds 2 edges (runCallThis→invoker, invoker→handler) and removes
131131
// the false-positive from handler being extracted as a callback arg of .call() (#1405) → 41.
132-
// #1407 adds class-scope.js (bare-call guard), +1 → total 42.
132+
// #1422 adds class-scope.js (bare-call guard), +1 → total 42.
133133
javascript: { precision: 1.0, recall: 0.9 },
134134
// pts-javascript: hand-authored points-to JS fixture (for-of, Set, Array.from, spread) — patterns
135135
// too broad for the main JS fixture. Patterns split per file to prevent intra-fixture FPs.

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', () => {

tests/unit/call-resolver.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* method fallback must apply computeConfidence >= 0.5 to avoid false edges from
1111
* distant files in a polyglot project.
1212
*
13-
* Also covers the bare-call JS/TS module-scope guard (#1407): bare `foo()` calls
13+
* Also covers the bare-call JS/TS module-scope guard (#1422/#1424): bare `foo()` calls
1414
* (no receiver) inside a JS/TS class method must NOT fall through to the same-class
1515
* lookup, because bare calls in those languages are module-scoped, not class-scoped.
1616
*/

0 commit comments

Comments
 (0)