Skip to content

Commit 6b2dfa0

Browse files
Synvoyasafishamsi
authored andcommitted
fix(extract): emit inherits edge for JavaScript class extends (#1790)
A plain JavaScript `class Dog extends Animal` produced no inherits edge — only the TypeScript extends_clause/implements_clause shapes were handled, not the bare identifier directly under class_heritage that JS emits. Fall back to reading heritage identifiers when no clause is present, so JS class inheritance is captured; a dynamic base (mixin call) yields nothing and TS is not double-emitted.
1 parent b27f219 commit 6b2dfa0

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

‎graphify/extractors/resolution.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,19 +1397,34 @@ def _ts_walk_class_members(class_node, source: bytes, path: Path, class_nid: str
13971397
line = class_node.start_point[0] + 1
13981398
for child in class_node.children:
13991399
if child.type == "class_heritage":
1400+
saw_clause = False
14001401
for clause in child.children:
14011402
if clause.type == "extends_clause":
1403+
saw_clause = True
14021404
for name in _ts_heritage_clause_entries(clause, source):
14031405
facts.uses.append(
14041406
_SymbolUseFact(path, class_nid, name, "inherits", "type",
14051407
clause.start_point[0] + 1)
14061408
)
14071409
elif clause.type == "implements_clause":
1410+
saw_clause = True
14081411
for name in _ts_heritage_clause_entries(clause, source):
14091412
facts.uses.append(
14101413
_SymbolUseFact(path, class_nid, name, "implements", "type",
14111414
clause.start_point[0] + 1)
14121415
)
1416+
if not saw_clause:
1417+
# The JavaScript grammar carries the base directly under
1418+
# class_heritage (`extends Base` -> [extends, identifier]) with no
1419+
# extends_clause/implements_clause wrapper like the TypeScript
1420+
# grammar. Treat the heritage node itself as the extends clause so
1421+
# `class Derived extends Base {}` in a .js file still emits an
1422+
# inherits edge. Mirrors the extends_type_clause branch below.
1423+
for name in _ts_heritage_clause_entries(child, source):
1424+
facts.uses.append(
1425+
_SymbolUseFact(path, class_nid, name, "inherits", "type",
1426+
child.start_point[0] + 1)
1427+
)
14131428
elif child.type == "extends_type_clause":
14141429
# Interface heritage (`interface A extends B, C`) is an
14151430
# extends_type_clause node, NOT a class_heritage. Its base entries

‎tests/test_ts_inheritance.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,42 @@ def test_class_extends_same_file(tmp_path):
5656
assert _has_inherits(result, "src/a.ts", "Dog", "src/a.ts", "Animal")
5757

5858

59+
def test_js_class_extends_same_file(tmp_path):
60+
"""`class Dog extends Animal {}` in a plain .js file must emit an inherits edge.
61+
62+
The JavaScript grammar stores the base identifier directly under the
63+
`class_heritage` node (no `extends_clause` wrapper as in the TypeScript
64+
grammar), so the heritage walk dropped the edge for .js classes while the
65+
equivalent .ts file (test_class_extends_same_file) already worked.
66+
"""
67+
f = _write(tmp_path / "src" / "a.js",
68+
"class Animal {}\n"
69+
"class Dog extends Animal {}\n")
70+
result = extract([f], cache_root=tmp_path)
71+
assert _has_inherits(result, "src/a.js", "Dog", "src/a.js", "Animal")
72+
73+
74+
def test_js_class_extends_imported(tmp_path):
75+
"""The JavaScript grammar path must retain import-guided resolution."""
76+
base = _write(tmp_path / "src" / "base.js", "export class Animal {}\n")
77+
derived = _write(tmp_path / "src" / "derived.js",
78+
"import { Animal } from './base.js';\n"
79+
"export class Dog extends Animal {}\n")
80+
result = extract([derived, base], cache_root=tmp_path)
81+
assert _has_inherits(result, "src/derived.js", "Dog", "src/base.js", "Animal")
82+
83+
84+
def test_js_dynamic_class_base_does_not_fabricate_inherits(tmp_path):
85+
"""A runtime base expression has no statically resolvable parent symbol."""
86+
f = _write(tmp_path / "src" / "a.js",
87+
"class Animal {}\n"
88+
"function mixin(base) { return class extends base {}; }\n"
89+
"class Dog extends mixin(Animal) {}\n")
90+
result = extract([f], cache_root=tmp_path)
91+
assert not _has_inherits(result, "src/a.js", "Dog", "src/a.js", "Animal")
92+
assert not _has_inherits(result, "src/a.js", "Dog", "src/a.js", "mixin")
93+
94+
5995
def test_interface_extends_generic_base_same_file(tmp_path):
6096
f = _write(tmp_path / "src" / "a.ts",
6197
"interface Base<T> { x: T; }\n"

0 commit comments

Comments
 (0)