Skip to content

JS/TS: override-dispatch edges link a static method to an instance method of the same name #2761

Closed

Description

@inth3shadows

On current main (847f1df), the override-dispatch edges (synthesizedBy: interface-impl) link a base-class method to a subclass method of the same name even when one is static and the other is not. A static method doesn't override an instance method (or the reverse), so a call to one never runs the other.

TypeScript, a.ts:

class B { s(): void {} }
class A extends B {
  static s(): void {}
}

Result: B::s -> A::s (calls, provenance: heuristic, synthesizedBy: interface-impl). Expected: no edge. The nodes do record the difference (B::s has is_static = 0, A::s has is_static = 1); the synthesizer matches by name only (

const impls = implsByKey.get(keyOf(bm));
if (!impls) {
if (promotes) unmatched.push(bm);
continue;
}
for (const m of impls) {
if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
if (bm.id === m.id) continue;
const key = `${bm.id}>${m.id}`;
if (seen.has(key)) continue;
seen.add(key);
edges.push({
source: bm.id,
target: m.id,
kind: 'calls',
line: bm.startLine,
provenance: 'heuristic',
metadata: { synthesizedBy: 'interface-impl', via: m.name, registeredAt: `${m.filePath}:${m.startLine}` },
});
).

JavaScript has the same edge, plus a second cause: JS methods never get isStatic at all. javascriptExtractor has no isStatic hook (

export const javascriptExtractor: LanguageExtractor = {
); TypeScript's has one (
isStatic: (node) => {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'static') return true;
}
return false;
},
), and the native kernel's is_static returns None for the JS variants (
/// TS has an isStatic hook; JS does not (None = field absent).
fn is_static(&self, node: Node) -> Option<bool> {
if self.variant.is_ts() {
Some(self.has_keyword_child(node, "static"))
} else {
None
}
). With

class B { static s() {} s() {} }
class A extends B { static s() {} }

both B::s nodes are stored with is_static = 0, and both get an edge to A::s. So a check in the synthesizer alone wouldn't cover JS.

Likely fix (inferred, not tried): give the JS extractor the same isStatic hook as TypeScript (and the kernel's JS path the same answer, for parity), then skip a base/override pair in the interface-impl pass when their isStatic differs. JS isStatic would also reach the other places that read it, which may change some JS resolution.

Searched open issues and PRs for interface-impl, static and override: none cover this. #2505 handles super.m() calls and static/instance in call resolution, not these synthesized edges.

Repro: index either file with codegraph init, then list calls edges from the DB (Linux / WSL2, Node 22).

Activity

  1. yang0228 commented on Oct 22, 2026

    @yang0228

    Reproduced on main at 0de94978 and opened #2775 with a fix covering both causes.

    JavaScript now records static modifiers through both the WASM extractor and the native JS/JSX kernel, using the same check as TypeScript. The JS/TS override-dispatch pass skips pairs whose static flags differ, while preserving same-kind matches. Regression coverage includes both mismatch directions, classes with static and instance methods sharing a name, callable class fields, and a Java control to keep other languages' matching unchanged.

    Validation: the full suite passed 8,447 tests with 27 conditional/platform skips; the actual native kernel was rebuilt and tested for extraction parity, and the WASM path was tested separately. All 15 new regression/control cases also passed on Node 22, and the production build passed. Details are in the PR.

    Re-index JavaScript and TypeScript projects after upgrading. Validation was on macOS with Node 22 and 24; Linux/WSL was not exercised directly.

  2. added a commit that references this issue on Oct 22, 2026
    e734449
  3. colbymchenry commented on Oct 22, 2026

    @colbymchenry
    Owner

    Thanks for the clear report and the two repros, @inth3shadows. Both causes were as you described: the override pass matched methods by name alone, and JavaScript members never recorded static, in either the extractor or the native kernel.

    #2813 is merged to main and fixes both. In JavaScript and TypeScript, a static method now pairs only with a static one, and an instance method only with an instance one. On webpack's source it removes 15 such links, like Module's deserialize(context) reaching the subclasses' static deserialize(context) factories, and changes no other edges.

    Thanks also to @yang0228, whose #2775 took the same approach. It ships in the next release; re-index JavaScript and TypeScript projects after upgrading.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions