Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 53 additions & 10 deletions crates/codegraph-core/src/extractors/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1751,17 +1751,29 @@ fn handle_call_expr(
handle_dynamic_import(node, &fn_node, source, symbols);
return;
}
// `this(args)` and `super(args)` — the callee is `this`/`super` used as a
// function, not a named identifier. The `this` call record is emitted by
// `this(args)` — the callee is `this` used as a function, not a named
// identifier. The `this` call record is emitted by
// collect_this_call_and_bindings (called from match_js_pts_bindings).
// Neither case should emit callback-reference calls for the arguments, because
// those arguments are values passed *to* the rebound function — not callbacks
// of the enclosing scope. Without this guard, identifier arguments like `b`
// in `this(b)` or `a` in `super(a)` become spurious dynamic calls that the
// pts resolver resolves to globally-defined functions with the same name in
// other files, producing false cross-file call edges.
// Mirrors the early-return guard in the TS handleCallExpr (javascript.ts:1135).
if fn_node.kind() == "this" || fn_node.kind() == "super" {
// Callback-reference-call extraction is skipped for the arguments, because
// those arguments are values passed *to* the rebound function — not
// callbacks of the enclosing scope. Without this guard, an identifier
// argument like `b` in `this(b)` becomes a spurious dynamic call that the
// pts resolver resolves to a globally-defined function with the same name
// in another file, producing a false cross-file call edge.
if fn_node.kind() == "this" {
return;
}
// Bare `super(args)` — invokes the parent class's constructor. Modeled as
// a `constructor` call with receiver `super` (mirrors the `super` branch
// in extractCallInfo, src/extractors/javascript.ts) so it flows through
// the same this/super hierarchy dispatch that already resolves
// `super.method()` to the parent class (#1929). Callback-reference-call
// extraction on the arguments is skipped for the same reason as
// `this(args)` above.
if fn_node.kind() == "super" {
if let Some(call_info) = extract_call_info(&fn_node, node, source) {
symbols.calls.push(call_info);
}
return;
}
// RES-2: {a:fnA,b:fnB}[k]() — inline object literal dispatch table.
Expand Down Expand Up @@ -3412,6 +3424,14 @@ fn extract_call_info(fn_node: &Node, call_node: &Node, source: &[u8]) -> Option<
}
None
}
// Bare `super(...)` — see the early dispatch in `handle_call_expr` for why
// callback-reference-call extraction is skipped for the arguments here.
"super" => Some(Call {
name: "constructor".to_string(),
line: start_line(call_node),
receiver: Some("super".to_string()),
..Default::default()
}),
_ => None,
}
}
Expand Down Expand Up @@ -6799,6 +6819,29 @@ mod tests {
);
}

/// Bare `super(...)` must be extracted as a `constructor` call with
/// receiver `super`, mirroring `super.method()` — the this/super hierarchy
/// dispatch (WASM-mirrored `resolveThisDispatch`) then attributes it to the
/// parent class's constructor (#1929).
#[test]
fn bare_super_call_extracted_as_constructor_call() {
let s = parse_js(
"class E { constructor(c) { this.cc = c; } }\n\
class G extends E {\n\
constructor(a) { super(a); }\n\
}",
);
let super_call = s
.calls
.iter()
.find(|c| c.name == "constructor" && c.receiver.as_deref() == Some("super"));
assert!(
super_call.is_some(),
"bare super(...) must be recorded as a constructor call with receiver=super; got: {:?}",
s.calls.iter().map(|c| (&c.name, &c.receiver)).collect::<Vec<_>>()
);
}

#[test]
fn array_elem_bindings_recorded() {
let s = parse_js(
Expand Down
1 change: 1 addition & 0 deletions src/domain/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const COMMON_QUERY_PATTERNS: string[] = [
'(call_expression function: (identifier) @callfn_name) @callfn_node',
'(call_expression function: (member_expression) @callmem_fn) @callmem_node',
'(call_expression function: (subscript_expression) @callsub_fn) @callsub_node',
'(call_expression function: (super) @callsuper_fn) @callsuper_node',
'(new_expression constructor: (identifier) @newfn_name) @newfn_node',
'(new_expression constructor: (member_expression) @newmem_fn) @newmem_node',
'(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
Expand Down
1 change: 1 addition & 0 deletions src/domain/wasm-worker-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const COMMON_QUERY_PATTERNS: string[] = [
'(call_expression function: (identifier) @callfn_name) @callfn_node',
'(call_expression function: (member_expression) @callmem_fn) @callmem_node',
'(call_expression function: (subscript_expression) @callsub_fn) @callsub_node',
'(call_expression function: (super) @callsuper_fn) @callsuper_node',
'(new_expression constructor: (identifier) @newfn_name) @newfn_node',
'(new_expression constructor: (member_expression) @newmem_fn) @newmem_node',
'(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
Expand Down
13 changes: 13 additions & 0 deletions src/extractors/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,11 @@ function dispatchQueryMatch(
} else if (c.newmem_node) {
const callInfo = extractCallInfo(c.newmem_fn!, c.newmem_node);
if (callInfo) calls.push(callInfo);
} 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) {
Comment on lines +390 to 395

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 TS/Rust divergence: super() args still pass through extractCallbackReferenceCalls

The Rust path explicitly returns early after pushing the constructor call, skipping extract_callback_reference_calls for super() arguments (with the comment "skipped for the same reason as this(args) above"). In the TS walk path handleCallExpr, there is no early return for fn.type === 'super' — it falls through to line 1714, and now the query path (dispatchQueryMatch) also explicitly calls extractCallbackReferenceCalls here, making both TS paths consistent with each other. However, if a constructor param is function-typed — e.g., super(onInit) where onInit matches a callback-shaped parameter — the TS engine will emit a spurious callback-reference call while the Rust engine won't. The new test covers only non-callback-typed params, so this case goes undetected. Consider either adding an early-return guard in the walk path for fn.type === 'super' (mirroring Rust), or adding a parity fixture that includes a function-typed super() argument to document the intended cross-engine behaviour.

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!

Fix in Claude Code

handleCommonJSAssignment(c.assign_left!, c.assign_right!, c.assign_node, imports);
handleFuncPropAssignment(c.assign_left!, c.assign_right!, definitions);
Expand Down Expand Up @@ -3577,6 +3582,14 @@ function extractCallInfo(
if (fnType === 'subscript_expression') {
return extractSubscriptCallInfo(fn, callNode, arrayElemBindings);
}
if (fnType === 'super') {
// Bare `super(...)` — invokes the parent class's constructor. Modeled as a
// `constructor` call with receiver `super` so it flows through the same
// this/super hierarchy dispatch as `super.method()` (resolveThisDispatch
// in cha.ts walks to the caller's parent class and looks up
// `ParentClass.constructor`), rather than needing a bespoke resolution path.
return { name: 'constructor', line: nodeStartLine(callNode), receiver: 'super' };
}
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,20 @@
"mode": "class-inheritance",
"notes": "static super.count() in DoubleCounter.count resolves to Counter.count via CHA parents map"
},
{
"source": { "name": "Car.constructor", "file": "inheritance.js" },
"target": { "name": "Vehicle.constructor", "file": "inheritance.js" },
"kind": "calls",
"mode": "class-inheritance",
"notes": "bare super(make) in Car.constructor resolves to Vehicle.constructor via CHA parents map (#1929)"
},
{
"source": { "name": "SportsCar.constructor", "file": "inheritance.js" },
"target": { "name": "Car.constructor", "file": "inheritance.js" },
"kind": "calls",
"mode": "class-inheritance",
"notes": "bare super(make, model) in SportsCar.constructor resolves to Car.constructor (nearest parent) (#1929)"
},
{
"source": { "name": "runPrototypes", "file": "prototypes.js" },
"target": { "name": "C", "file": "prototypes.js" },
Expand Down
22 changes: 22 additions & 0 deletions tests/benchmarks/resolution/fixtures/javascript/inheritance.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,25 @@ export class DoubleCounter extends Counter {
return super.count() * 2; // static super.count() → Counter.count via CHA parents map
}
}

// Bare super(...) constructor call — same CHA parents-map resolution as
// super.method(), but for the keyword-callee constructor invocation (#1929)
export class Vehicle {
constructor(make) {
this.make = make;
}
}

export class Car extends Vehicle {
constructor(make, model) {
super(make); // bare super(...) → Vehicle.constructor
this.model = model;
}
}

export class SportsCar extends Car {
constructor(make, model, topSpeed) {
super(make, model); // bare super(...) → Car.constructor (nearest parent)
this.topSpeed = topSpeed;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@
"mode": "receiver-typed",
"notes": "shape.describe() — shape typed as Shape"
},
{
"source": { "name": "Ellipse.constructor", "file": "hierarchy.ts" },
"target": { "name": "Circle.constructor", "file": "hierarchy.ts" },
"kind": "calls",
"mode": "class-inheritance",
"notes": "bare super(rx) in Ellipse.constructor resolves to Circle.constructor via CHA parents map (#1929). Circle/Rectangle's own super() calls resolve to Shape's implicit default constructor, which has no declared node to attribute to, so they correctly produce no edge."
},
{
"source": { "name": "makeCircle", "file": "hierarchy.ts" },
"target": { "name": "Circle", "file": "hierarchy.ts" },
Expand Down Expand Up @@ -331,6 +338,13 @@
"kind": "calls",
"mode": "class-inheritance",
"notes": "super.log() in PrefixLogger.log resolves to TimestampLogger.log (nearest parent)"
},
{
"source": { "name": "Box.constructor", "file": "super-dispatch.ts" },
"target": { "name": "Container.constructor", "file": "super-dispatch.ts" },
"kind": "calls",
"mode": "class-inheritance",
"notes": "bare super(label) in Box.constructor resolves to Container.constructor via CHA parents map (#1929)"
}
]
}
17 changes: 17 additions & 0 deletions tests/benchmarks/resolution/fixtures/typescript/super-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,20 @@ export class PrefixLogger extends TimestampLogger {
super.log(`[PREFIX] ${msg}`); // super.method() → TimestampLogger.log (nearest parent)
}
}

// Bare super(...) constructor call — same CHA parents-map resolution as
// super.method(), but for the keyword-callee constructor invocation (#1929).
// Named distinctly from hierarchy.ts's Shape/Circle/Rectangle/Ellipse to avoid
// cross-file base-class name collisions in the global CHA parents map.
export class Container {
constructor(public label: string) {}
}

export class Box extends Container {
constructor(
label: string,
public sides: number,
) {
super(label); // bare super(...) → Container.constructor
}
}
20 changes: 20 additions & 0 deletions tests/engines/query-walk-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,26 @@ function fnB() {}
function run(key) {
return ({ a: fnA, b: fnB })[key]();
}
`,
},
{
// Regression guard for #1929: bare `super(...)` constructor calls must be
// extracted as a `constructor` call with receiver `super` on both paths —
// the query path via the new `callsuper_node` capture in
// `dispatchQueryMatch`, the walk path via `handleCallExpr`'s fall-through
// to `extractCallInfo`'s `super` branch.
name: 'bare super(...) constructor call (#1929)',
file: 'test.js',
code: `
class Base {
constructor(a) { this.a = a; }
}
class Derived extends Base {
constructor(a, b) {
super(a);
this.b = b;
}
}
`,
},
// TSX
Expand Down
11 changes: 11 additions & 0 deletions tests/fixtures/cha-dispatch/Car.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Vehicle } from './Vehicle.js';

export class Car extends Vehicle {
model: string;

constructor(make: string, model: string) {
// super-dispatch: bare super(...) resolves to Vehicle.constructor via parents map (#1929).
super(make);
this.model = model;
}
}
7 changes: 7 additions & 0 deletions tests/fixtures/cha-dispatch/Vehicle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class Vehicle {
make: string;

constructor(make: string) {
this.make = make;
}
}
15 changes: 15 additions & 0 deletions tests/integration/phase-8.5-cha-dispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,21 @@ describe.each(ENGINES)('Phase 8.5 CHA dispatch (%s)', (engine) => {
).toBeUndefined();
});

// ── bare super(...) constructor call (issue #1929) ──────────────────────

it('super-dispatch: emits Car.constructor → Vehicle.constructor (bare super(...) call)', () => {
const edge = callEdges.find(
(e) =>
e.caller_name === 'Car.constructor' &&
e.callee_name === 'Vehicle.constructor' &&
e.callee_file === 'Vehicle.ts',
);
expect(
edge,
`Expected Car.constructor → Vehicle.constructor edge (bare super(...) dispatch via class hierarchy).\nActual edges:\n${JSON.stringify(callEdges, null, 2)}`,
).toBeDefined();
});

// ── transitive multi-level CHA (issue #1311) ───────────────────────────
// Hierarchy: IJob → AbstractJob (non-instantiated) → PrintJob / ScanJob
// resolveChaTargets must BFS through AbstractJob to reach the concrete types.
Expand Down
33 changes: 33 additions & 0 deletions tests/parsers/javascript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2373,6 +2373,39 @@ function runDemo(users: string[]): void {
});
});

describe('bare super(...) constructor call extraction (#1929)', () => {
it('records bare super(...) as a constructor call with receiver=super', () => {
const symbols = parseJS(
`class Base { constructor(a) { this.a = a; } }
class Derived extends Base { constructor(a, b) { super(a); this.b = b; } }`,
);
const superCall = symbols.calls.find(
(c) => c.name === 'constructor' && c.receiver === 'super',
);
expect(superCall).toBeDefined();
expect(superCall!.dynamic).toBeFalsy();
});

it('records bare super(...) from a class expression constructor', () => {
const symbols = parseJS(
`function mixin() { return class PostMixin extends A { constructor() { super(); } }; }`,
);
const superCall = symbols.calls.find(
(c) => c.name === 'constructor' && c.receiver === 'super',
);
expect(superCall).toBeDefined();
});

it('does not emit super(...) arguments as spurious callback-reference calls', () => {
const symbols = parseJS(
`class Base { constructor(a, b) { this.a = a; this.b = b; } }
class Derived extends Base { constructor(a, b) { super(a, b); } }`,
);
expect(symbols.calls.some((c) => c.name === 'a')).toBe(false);
expect(symbols.calls.some((c) => c.name === 'b')).toBe(false);
});
});

describe('array destructuring constant extraction (#1471, #1901)', () => {
it('extracts one constant definition per bound identifier in a const array pattern', () => {
// Per-element extraction (#1901) supersedes the prior single-node
Expand Down
Loading