fix(extractors): extract bare super(...) constructor calls as call edges#2102
Conversation
extractCallInfo (WASM/TS) and extract_call_info (native Rust) had no branch for the super keyword-callee call shape, so a bare super(...) constructor call was silently dropped -- unlike super.method(), which already resolves via the this/super hierarchy dispatch. Any base class reached only through subclass constructors (never directly new'd) was misclassified dead-unresolved. Model super(...) as a constructor call with receiver=super so it flows through the existing resolveThisDispatch machinery unchanged. Adds the matching (call_expression function: (super)) query pattern (mirrored in parser.ts and wasm-worker-entry.ts) so the WASM worker's query path extracts it identically to the walk path, and mirrors the native extractor so both engines produce the same Subclass.constructor -> ParentClass.constructor edge. (cherry picked from commit 5522bc1)
Greptile SummaryThis PR fixes a long-standing gap where bare
Confidence Score: 5/5Safe to merge — the fix is a straightforward additive branch in all three extraction paths with no changes to existing resolution logic. All three extraction paths (TS walk, TS query, Rust native) receive identical treatment: an explicit early-return that emits the constructor call record and skips callback-reference extraction, matching the documented this(args) precedent. The change is purely additive and test coverage is thorough across unit, parity, integration, and benchmark fixture levels. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["super(args) call site"] --> B{Extraction path}
B --> C["Walk path (handleCallExpr)"]
B --> D["Query path (dispatchQueryMatch)"]
B --> E["Rust native path (handle_call_expr)"]
C --> F{"fn.type === 'super'?"}
F -- yes --> G["extractCallInfo → {name:'constructor', receiver:'super'}"]
G --> H["ctx.calls.push — skip callback refs"]
D --> I["callsuper_node capture"]
I --> J["extractCallInfo → {name:'constructor', receiver:'super'}"]
J --> K["calls.push — no extractCallbackReferenceCalls"]
E --> L{"fn_node.kind() == 'super'?"}
L -- yes --> M["extract_call_info → Call{name:'constructor', receiver:Some('super')}"]
M --> N["symbols.calls.push — skip callback refs"]
H --> O["resolveThisDispatch (same path as super.method())"]
K --> O
N --> O
O --> P["Look up parent class in CHA parents map"]
P --> Q["Emit: Subclass.constructor → ParentClass.constructor edge"]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["super(args) call site"] --> B{Extraction path}
B --> C["Walk path (handleCallExpr)"]
B --> D["Query path (dispatchQueryMatch)"]
B --> E["Rust native path (handle_call_expr)"]
C --> F{"fn.type === 'super'?"}
F -- yes --> G["extractCallInfo → {name:'constructor', receiver:'super'}"]
G --> H["ctx.calls.push — skip callback refs"]
D --> I["callsuper_node capture"]
I --> J["extractCallInfo → {name:'constructor', receiver:'super'}"]
J --> K["calls.push — no extractCallbackReferenceCalls"]
E --> L{"fn_node.kind() == 'super'?"}
L -- yes --> M["extract_call_info → Call{name:'constructor', receiver:Some('super')}"]
M --> N["symbols.calls.push — skip callback refs"]
H --> O["resolveThisDispatch (same path as super.method())"]
K --> O
N --> O
O --> P["Look up parent class in CHA parents map"]
P --> Q["Emit: Subclass.constructor → ParentClass.constructor edge"]
Reviews (4): Last reviewed commit: "Merge remote-tracking branch 'origin/fix..." | Re-trigger Greptile |
| } else if (c.callsuper_node) { | ||
| // Bare `super(...)` constructor call — see extractCallInfo's 'super' branch. | ||
| const callInfo = extractCallInfo(c.callsuper_fn!, c.callsuper_node); | ||
| if (callInfo) calls.push(callInfo); | ||
| calls.push(...extractCallbackReferenceCalls(c.callsuper_node, callbackParamShapes)); | ||
| } else if (c.assign_node) { |
There was a problem hiding this comment.
The
callsuper_node branch explicitly calls extractCallbackReferenceCalls, which contradicts the documented intent — both the PR description and the Rust extractor's comment explicitly state that callback-reference processing of super() arguments must be skipped (for the same reason as this(args)). Today this is harmless because extractCalleeName returns null for a super callee node, causing extractCallbackReferenceCalls to short-circuit and return []. However, it creates a silent semantic divergence from the Rust native path: if extractCalleeName or the allowlists change to recognize super as a named callee, the query path would start emitting spurious callback edges that the walk path and Rust extractor would not.
| } else if (c.callsuper_node) { | |
| // Bare `super(...)` constructor call — see extractCallInfo's 'super' branch. | |
| const callInfo = extractCallInfo(c.callsuper_fn!, c.callsuper_node); | |
| if (callInfo) calls.push(callInfo); | |
| calls.push(...extractCallbackReferenceCalls(c.callsuper_node, callbackParamShapes)); | |
| } else if (c.assign_node) { | |
| } else if (c.callsuper_node) { | |
| // Bare `super(...)` constructor call — see extractCallInfo's 'super' branch. | |
| // Callback-reference-call extraction is intentionally skipped for the arguments: | |
| // they are values passed *to* the parent constructor, not callbacks of the enclosing | |
| // scope. Mirrors the explicit early-return in the Rust handle_call_expr super branch. | |
| const callInfo = extractCallInfo(c.callsuper_fn!, c.callsuper_node); | |
| if (callInfo) calls.push(callInfo); | |
| } else if (c.assign_node) { |
There was a problem hiding this comment.
Fixed in 12ab0d7 — removed the extractCallbackReferenceCalls call from the callsuper_node branch in dispatchQueryMatch (query path), matching the suggested diff exactly.
While fixing it, I found the walk path (handleCallExpr) had the identical latent bug: it lacked an early return for fn.type === 'super' (unlike the this branch right above it, which already returns early), so it fell through to the same unconditional extractCallbackReferenceCalls call at the end of the function. Added the matching early return there too, so both extraction paths — and the Rust native extractor, which already returns immediately after recording the call — are now consistent.
Codegraph Impact Analysis17 functions changed → 15 callers affected across 1 files
|
…#2102) Greptile flagged the callsuper_node branch in dispatchQueryMatch (query path) for still calling extractCallbackReferenceCalls on super()'s arguments, contradicting the documented intent and diverging from the Rust extractor's explicit early return. The walk path (handleCallExpr) had the same latent issue: it lacked an early return for fn.type === 'super', so it fell through to the same unconditional extractCallbackReferenceCalls call at the end of the function -- unlike the sibling `this` branch just above it, which does return early. Both are fixed the same way, mirroring the Rust handle_call_expr's super branch (which already returns immediately after recording the call). Impact: 2 functions changed, 4 affected
…edges' into fix/bare-super-constructor-call-edges Impact: 1 functions changed, 12 affected
|
Addressed Greptile's review feedback:
Also merged All local tests pass (4138 passed, 0 failed) and lint is clean. |
Summary
extractCallInfo(WASM/TS) andextract_call_info(native Rust) had no branch for thesuperkeyword-callee call shape, so a baresuper(...)constructor call was silently dropped — unlikesuper.method(), which already resolves via the this/super hierarchy dispatch. Any base class reached only through subclass constructors (never directlynew'd) was misclassified dead/unresolved.super(...)as a constructor call withreceiver=superso it flows through the existingresolveThisDispatchmachinery unchanged.(call_expression function: (super))query pattern (mirrored inparser.tsandwasm-worker-entry.ts) so the WASM worker's query path extracts it identically to the walk path, and mirrors the native extractor so both engines produce the sameSubclass.constructor -> ParentClass.constructoredge.Recreated from #2065 as a clean branch off current
main— the original branch had drifted ~120 commits behind and carried unrelated accumulated history unrelated to this fix (see #2101). This PR contains only the single commit that was the actual intended change.Test plan