Skip to content

fix(extractors): extract bare super(...) constructor calls as call edges#2102

Merged
carlos-alm merged 6 commits into
mainfrom
fix/bare-super-constructor-call-edges
Jul 17, 2026
Merged

fix(extractors): extract bare super(...) constructor calls as call edges#2102
carlos-alm merged 6 commits into
mainfrom
fix/bare-super-constructor-call-edges

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • 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.
  • Models 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.

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

  • Cherry-picked cleanly against current main, no conflicts
  • CI green (build, tests, engine parity)

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-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a long-standing gap where bare super(...) constructor calls were silently dropped by both the TS (WASM) and Rust (native) extractors — unlike super.method(), which already resolved via the resolveThisDispatch machinery. The fix models super(...) as a { name: 'constructor', receiver: 'super' } call, letting it flow through the existing CHA parents-map dispatch without a bespoke resolution path.

  • Adds a "super" arm to extractCallInfo in both the TS and Rust extractors, and an explicit early-return guard in each handleCallExpr to skip spurious callback-reference-call extraction on the arguments (matching the existing this(args) guard).
  • Adds the (call_expression function: (super) @callsuper_fn) @callsuper_node tree-sitter query pattern to parser.ts and wasm-worker-entry.ts, and a matching callsuper_node branch in dispatchQueryMatch, so the WASM query path is parity-identical to the walk path.
  • Covers the fix with unit tests (javascript.test.ts), a query-walk parity regression guard, an integration test (phase-8.5-cha-dispatch.test.ts), and updated benchmark expected-edges for both JS and TS fixtures.

Confidence Score: 5/5

Safe 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

Filename Overview
src/extractors/javascript.ts Adds the super arm to extractCallInfo, an explicit early-return branch in handleCallExpr (walk path), and a callsuper_node branch in dispatchQueryMatch (query path); all three are consistent and correctly skip callback-reference extraction.
crates/codegraph-core/src/extractors/javascript.rs Mirrors the TS changes: splits the old combined `this
src/domain/parser.ts Adds the callsuper_fn / callsuper_node query pattern; identical to the same addition in wasm-worker-entry.ts, keeping the two pattern lists in sync.
src/domain/wasm-worker-entry.ts Mirrors parser.ts — adds the callsuper_fn / callsuper_node query pattern so the WASM worker sees bare super(...) calls identically.
tests/engines/query-walk-parity.test.ts Adds a regression guard for #1929 ensuring the constructor/receiver=super call record appears on both extraction paths.
tests/integration/phase-8.5-cha-dispatch.test.ts Adds an end-to-end integration assertion that Car.constructor → Vehicle.constructor is emitted for both WASM and native engines.
tests/parsers/javascript.test.ts Three new parser-level unit tests: correct call record shape, class-expression support, and the no-spurious-callback guard.
tests/benchmarks/resolution/fixtures/javascript/expected-edges.json Adds two expected Car→Vehicle and SportsCar→Car constructor edges to match the new fixture classes in inheritance.js.
tests/benchmarks/resolution/fixtures/typescript/expected-edges.json Adds two expected edges: Ellipse.constructor→Circle.constructor (existing hierarchy.ts, previously dropped) and Box.constructor→Container.constructor (new super-dispatch.ts fixture).

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"]
Loading
%%{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"]
Loading

Reviews (4): Last reviewed commit: "Merge remote-tracking branch 'origin/fix..." | Re-trigger Greptile

Comment on lines +390 to 395
} 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
} 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) {

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

github-actions Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

17 functions changed15 callers affected across 1 files

  • dispatchQueryMatch in src/extractors/javascript.ts:326 (2 transitive callers)
  • handleCallExpr in src/extractors/javascript.ts:1687 (3 transitive callers)
  • extractCallInfo in src/extractors/javascript.ts:3581 (20 transitive callers)
  • Vehicle in tests/benchmarks/resolution/fixtures/javascript/inheritance.js:38 (0 transitive callers)
  • Vehicle.constructor in tests/benchmarks/resolution/fixtures/javascript/inheritance.js:39 (0 transitive callers)
  • Car in tests/benchmarks/resolution/fixtures/javascript/inheritance.js:44 (0 transitive callers)
  • Car.constructor in tests/benchmarks/resolution/fixtures/javascript/inheritance.js:45 (0 transitive callers)
  • SportsCar in tests/benchmarks/resolution/fixtures/javascript/inheritance.js:51 (0 transitive callers)
  • SportsCar.constructor in tests/benchmarks/resolution/fixtures/javascript/inheritance.js:52 (0 transitive callers)
  • Container in tests/benchmarks/resolution/fixtures/typescript/super-dispatch.ts:27 (0 transitive callers)
  • Container.constructor in tests/benchmarks/resolution/fixtures/typescript/super-dispatch.ts:28 (0 transitive callers)
  • Box in tests/benchmarks/resolution/fixtures/typescript/super-dispatch.ts:31 (0 transitive callers)
  • Box.constructor in tests/benchmarks/resolution/fixtures/typescript/super-dispatch.ts:32 (0 transitive callers)
  • Car in tests/fixtures/cha-dispatch/Car.ts:3 (0 transitive callers)
  • Car.constructor in tests/fixtures/cha-dispatch/Car.ts:6 (0 transitive callers)
  • Vehicle in tests/fixtures/cha-dispatch/Vehicle.ts:1 (0 transitive callers)
  • Vehicle.constructor in tests/fixtures/cha-dispatch/Vehicle.ts:4 (0 transitive callers)

carlos-alm and others added 4 commits July 16, 2026 02:31
…#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
@carlos-alm

Copy link
Copy Markdown
Contributor Author

Addressed Greptile's review feedback:

  • Removed the extractCallbackReferenceCalls call from the callsuper_node branch in dispatchQueryMatch (query path), per the suggested diff.
  • Also fixed the identical latent issue in the walk path (handleCallExpr), which lacked the equivalent early return for fn.type === 'super' and fell through to the same unconditional callback-extraction call. Both extraction paths now match the Rust native extractor's explicit early return.

Also merged main twice: once for a benchmark-baseline-drift fix already landed there (unrelated CI failure this branch was hitting), and once more to reconcile with another concurrent push to this same branch. Verified full diff against origin/main and ORIG_HEAD after each merge — no content lost.

All local tests pass (4138 passed, 0 failed) and lint is clean.

@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm
carlos-alm merged commit 156004a into main Jul 17, 2026
21 checks passed
@carlos-alm
carlos-alm deleted the fix/bare-super-constructor-call-edges branch July 17, 2026 06:13
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant