feat(resolver): track Function.bind/call/apply for receiver-typed resolution (JS)#1330
Conversation
…olution (JS)
- bind: var f = fn.bind(ctx) seeds fnRefBinding {lhs:f, rhs:fn} so pts(f) ⊇ pts(fn);
direct calls f() then resolve to fn via points-to.
- call/apply: f.call(ctx, args) and f.apply(ctx, args) already extract f as the
callee via extractMemberExprCallInfo; no new extraction needed.
- pts gate: extend build-edges to also check the flat unscoped pts key for
module-level alias bindings (case c) — safe because resolveViaPointsTo filters
self-references, so self-seeded definitions never produce spurious edges.
Adds bind-call-apply.js fixture; JS benchmark stays at 100% precision.
Closes #1318
docs check acknowledged
|
Claude finished @carlos-alm's task —— View job PR Review: feat(resolver): track Function.bind/call/apply for receiver-typed resolution (JS)Review complete.
Root Cause AnalysisProblem Being Solved: JS functions aliased via Why This Approach: Correct and principled. Risk Assessment: Low. The bind-detection code is narrowly scoped: it only triggers on Phase 2: Configuration Change DetectionNo linting rules, TypeScript flags, test timeouts, or coverage thresholds were modified. ✓ Phase 3: Test Integrity
No red flags. ✓ Phase 4: Code Quality
The bind detection is correct and well-guarded: } else if (valueN.type === 'call_expression') {
const callFn = valueN.childForFieldName('function');
if (callFn?.type === 'member_expression') {
const bindProp = callFn.childForFieldName('property');
if (bindProp?.text === 'bind') {
const boundFn = callFn.childForFieldName('object');
if (boundFn?.type === 'identifier' && !BUILTIN_GLOBALS.has(boundFn.text)) {
fnRefBindings.push({ lhs: nameN.text, rhs: boundFn.text });
}
}
}
}One observation:
The new case (c) in the pts gate comment ( const flatPtsKey = !call.dynamic && ptsMap?.has(call.name) ? call.name : null;
if (
targets.length === 0 &&
!call.receiver &&
ptsMap &&
(call.dynamic || (scopedPtsKey != null && ptsMap.has(scopedPtsKey)) || flatPtsKey != null)
) {
const ptsLookupName = call.dynamic
? call.name
: scopedPtsKey != null && ptsMap.has(scopedPtsKey)
? scopedPtsKey
: (flatPtsKey ?? call.name);Minor concern: the condition Fixture ( Native engine coverage: The bind case seeds Phase 5: Backlog Compliance
Critical Concerns
Final Recommendation |
Greptile SummaryThis PR extends JS call-graph resolution to handle
Confidence Score: 5/5Safe to merge; the new bind-tracking path is narrowly guarded and cannot produce spurious edges for calls that already resolve directly. The extractor change is tightly bounded to fn.bind(ctx) on flat identifiers. The pts-gate addition is guarded by fnRefBindingLhs (a pre-computed set of only bind/alias lhs names), preventing the previously-raised over-broad ptsMap matching. The new native-path post-pass is a near-exact parallel of the existing buildParamFlowPtsPostPass with the same dedup and filtering guarantees. No files require special attention. Important Files Changed
Reviews (5): Last reviewed commit: "fix: resolve merge conflicts with main" | Re-trigger Greptile |
| const ptsLookupName = call.dynamic | ||
| ? call.name | ||
| : scopedPtsKey != null && ptsMap.has(scopedPtsKey) | ||
| ? scopedPtsKey | ||
| : (flatPtsKey ?? call.name); |
There was a problem hiding this comment.
The
?? call.name fallback in the ternary is unreachable. For the ternary to reach flatPtsKey ?? call.name, it must be non-dynamic AND scopedPtsKey is absent from the map. But if flatPtsKey is also null, the outer if condition can only be entered via scopedPtsKey != null && ptsMap.has(scopedPtsKey) — which takes the scopedPtsKey branch first. So flatPtsKey ?? call.name always evaluates to flatPtsKey in reachable code; the ?? call.name arm is dead.
| const ptsLookupName = call.dynamic | |
| ? call.name | |
| : scopedPtsKey != null && ptsMap.has(scopedPtsKey) | |
| ? scopedPtsKey | |
| : (flatPtsKey ?? call.name); | |
| const ptsLookupName = call.dynamic | |
| ? call.name | |
| : scopedPtsKey != null && ptsMap.has(scopedPtsKey) | |
| ? scopedPtsKey | |
| : flatPtsKey; |
There was a problem hiding this comment.
Fixed — removed the dead ?? call.name fallback. The ternary now ends with flatPtsKey! (non-null assertion), which is correct since the outer if condition guarantees flatPtsKey != null when neither call.dynamic nor scopedPtsKey branches match. Commit: 2466048.
| const flatPtsKey = !call.dynamic && ptsMap?.has(call.name) ? call.name : null; | ||
| if ( | ||
| targets.length === 0 && | ||
| !call.receiver && | ||
| ptsMap && | ||
| (call.dynamic || (scopedPtsKey != null && ptsMap.has(scopedPtsKey))) | ||
| (call.dynamic || (scopedPtsKey != null && ptsMap.has(scopedPtsKey)) || flatPtsKey != null) |
There was a problem hiding this comment.
Case (c) gate is broader than the comment implies
buildPointsToMap seeds every locally-defined function AND every imported name with a self-pointing entry (pts.set(name, new Set([name]))). As a result, ptsMap.has(call.name) is true for virtually any unresolved call, not only bind-seeded aliases — so flatPtsKey != null fires far more often than the "module-level alias bindings" comment suggests. Safety is maintained today because resolveViaPointsTo filters self-references, yielding [] for names whose only pts entry is themselves. However, this invariant breaks if a module has both a locally-defined function foo and an fnRefBinding with lhs = 'foo' (e.g. const foo = externalFn shadowing the declaration): pts('foo') would grow to {'foo', 'externalFn'}, and a failed direct call to foo() would produce a spurious edge to externalFn. A narrower guard — checking whether call.name appears as an lhs in fnRefBindings rather than in the full ptsMap — would restrict case (c) to exactly the bind/alias entries it intends to cover.
There was a problem hiding this comment.
Fixed — the flatPtsKey guard now checks fnRefBindingLhs.has(call.name) (a pre-computed set of lhs names from symbols.fnRefBindings) in addition to ptsMap.has(call.name). This restricts case (c) to exactly the names that appear as bind/alias lhs entries, preventing the over-broad match against self-seeded local definitions and imports. A parallel buildFnRefBindingsPtsPostPass was also added for the native engine path using the same narrowed guard. Commit: 2466048.
Codegraph Impact Analysis9 functions changed → 17 callers affected across 6 files
|
…bind aliases - Case (c) flatPtsKey guard now checks fnRefBindingLhs (lhs names from fnRefBindings) instead of the full ptsMap, preventing spurious edges when a locally-defined function shares a name with a bind/alias lhs. - Remove dead `?? call.name` fallback in the ptsLookupName ternary; use flatPtsKey! (non-null assertion) which is guaranteed by the outer if guard. - Add buildFnRefBindingsPtsPostPass for the native engine path so that bind-created aliases resolve on --engine native with the same correctness as the WASM/JS path.
|
Addressed Claude's review feedback:
All tests pass (171 resolution benchmark + 71 JS parser + 620 integration tests). Commit: 2466048. |
|
Claude finished @carlos-alm's task —— View job Re-Review: feat(resolver): track Function.bind/call/apply for receiver-typed resolution (JS)Reviewing commit 2466048 (follow-up fixes)
All three concerns from the initial review are correctly resolved.1. Native engine parity — The new post-pass mirrors
The only structural observation: 2. Case (c) guard narrowing — const fnRefBindingLhs = new Set(symbols.fnRefBindings?.map((b) => b.lhs) ?? []);
// ...
const flatPtsKey =
!call.dynamic && fnRefBindingLhs.has(call.name) && ptsMap?.has(call.name) ? call.name : null;This correctly fixes the over-broad
The shadowing concern Greptile raised ( 3. Dead code removal — : // flatPtsKey != null is guaranteed by the outer if condition: if neither
// call.dynamic nor scopedPtsKey matched, flatPtsKey != null must be true.
flatPtsKey!;Correct. The outer One minor observation (non-blocking)
const fnRefBindingLhs = new Set(symbols.fnRefBindings!.map((b) => b.lhs));This is fine — the Final AssessmentAll three critical concerns from the initial review are resolved cleanly. The implementation is now:
Rating: ⭐⭐⭐⭐⭐ (5/5) — APPROVE |
|
Addressed Greptile's follow-up suggestions from the review summary:
|
Summary
var f = fn.bind(ctx)now seeds afnRefBinding {lhs: f, rhs: fn}, so the points-to solver propagatespts(f) ⊇ pts(fn). Subsequent direct callsf()resolve tofnvia the pts path.fn.call(ctx, args)andfn.apply(ctx, argsArray)already extractedfnas the callee viaextractMemberExprCallInfo; no new extraction needed.bind) also resolve. Safe:resolveViaPointsTofilters self-references, so self-seeded locally-defined function names never produce spurious edges.Changes
src/extractors/javascript.tshandleVarDeclaratorTypeMapto detectfn.bind(ctx)call_expression and pushfnRefBinding {lhs, rhs: fn}src/domain/graph/builder/stages/build-edges.tstests/benchmarks/resolution/fixtures/javascript/bind-call-apply.jstests/benchmarks/resolution/fixtures/javascript/expected-edges.jsonTest plan
npm run lint— no errors in modified filesrunBind→greet(points-to),runCall→greet(dynamic),runApply→greet(dynamic)Closes #1318