diff --git a/graphify/extractors/resolution.py b/graphify/extractors/resolution.py index d46b4cbcc..4df3f47bd 100644 --- a/graphify/extractors/resolution.py +++ b/graphify/extractors/resolution.py @@ -1250,19 +1250,34 @@ def _ts_walk_class_members(class_node, source: bytes, path: Path, class_nid: str line = class_node.start_point[0] + 1 for child in class_node.children: if child.type == "class_heritage": + saw_clause = False for clause in child.children: if clause.type == "extends_clause": + saw_clause = True for name in _ts_heritage_clause_entries(clause, source): facts.uses.append( _SymbolUseFact(path, class_nid, name, "inherits", "type", clause.start_point[0] + 1) ) elif clause.type == "implements_clause": + saw_clause = True for name in _ts_heritage_clause_entries(clause, source): facts.uses.append( _SymbolUseFact(path, class_nid, name, "implements", "type", clause.start_point[0] + 1) ) + if not saw_clause: + # The JavaScript grammar carries the base directly under + # class_heritage (`extends Base` -> [extends, identifier]) with no + # extends_clause/implements_clause wrapper like the TypeScript + # grammar. Treat the heritage node itself as the extends clause so + # `class Derived extends Base {}` in a .js file still emits an + # inherits edge. Mirrors the extends_type_clause branch below. + for name in _ts_heritage_clause_entries(child, source): + facts.uses.append( + _SymbolUseFact(path, class_nid, name, "inherits", "type", + child.start_point[0] + 1) + ) elif child.type == "extends_type_clause": # Interface heritage (`interface A extends B, C`) is an # extends_type_clause node, NOT a class_heritage. Its base entries diff --git a/tests/test_ts_inheritance.py b/tests/test_ts_inheritance.py index cf7f8f2af..0e8810f5e 100644 --- a/tests/test_ts_inheritance.py +++ b/tests/test_ts_inheritance.py @@ -56,6 +56,21 @@ def test_class_extends_same_file(tmp_path): assert _has_inherits(result, "src/a.ts", "Dog", "src/a.ts", "Animal") +def test_js_class_extends_same_file(tmp_path): + """`class Dog extends Animal {}` in a plain .js file must emit an inherits edge. + + The JavaScript grammar stores the base identifier directly under the + `class_heritage` node (no `extends_clause` wrapper as in the TypeScript + grammar), so the heritage walk dropped the edge for .js classes while the + equivalent .ts file (test_class_extends_same_file) already worked. + """ + f = _write(tmp_path / "src" / "a.js", + "class Animal {}\n" + "class Dog extends Animal {}\n") + result = extract([f], cache_root=tmp_path) + assert _has_inherits(result, "src/a.js", "Dog", "src/a.js", "Animal") + + def test_interface_extends_generic_base_same_file(tmp_path): f = _write(tmp_path / "src" / "a.ts", "interface Base { x: T; }\n"