diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 90c95cc49..013e8923a 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -2344,8 +2344,16 @@ def walk(node, parent_class_nid: str | None = None) -> None: args = node.child_by_field_name("superclasses") if args: for arg in args.children: - if arg.type == "identifier": - base = _read_text(arg, source) + base_node = arg + # A parameterized base (`Base[T]`, `Generic[T]`, + # `Repo[User]`) parses as a `subscript` whose `value` + # field is the actual base type; the `[...]` holds only + # type parameters. Unwrap to the value so the heritage + # edge points at the base instead of being dropped. + if base_node.type == "subscript": + base_node = base_node.child_by_field_name("value") or base_node + if base_node.type == "identifier": + base = _read_text(base_node, source) base_nid = ensure_named_node(base, line) add_edge(class_nid, base_nid, "inherits", line) diff --git a/tests/test_languages.py b/tests/test_languages.py index f610fbc40..a08480662 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -3,6 +3,7 @@ from pathlib import Path import pytest from graphify.extract import ( + extract_python, extract_java, extract_c, extract_cpp, extract_ruby, extract_csharp, extract_kotlin, extract_scala, extract_php, extract_swift, extract_go, extract_julia, extract_js, extract_fortran, @@ -333,6 +334,33 @@ def test_ruby_inherits_edge(): assert found, "TimeoutApiClient should have inherits edge to ApiClient" +# ── Python ────────────────────────────────────────────────────────────────── + +def test_python_parameterized_base_inherits_edge(tmp_path): + """`class C(Base[T])` must still emit an inherits edge to the base type. + + The Python heritage handler only matched bare `identifier` bases in the + `superclasses` list. A parameterized base parses as a `subscript` node + (its `value` field is the base type; the `[...]` holds only type params), + so it fell through and the heritage edge was silently dropped -- every + `Generic[T]` / `Repo[User]` base lost its `inherits` edge. + """ + source = tmp_path / "heritage.py" + source.write_text( + "class Base: pass\n" + "class Mixin: pass\n" + "class Repo(Base[int]): pass\n" + "class Multi(Mixin, Base[str]): pass\n" + ) + result = extract_python(source) + inherits = _edge_labels(result, "inherits") + # Parameterized base must resolve to the base type, not be dropped. + assert ("Repo", "Base") in inherits + assert ("Multi", "Base") in inherits + # Bare-identifier bases must keep working (regression guard). + assert ("Multi", "Mixin") in inherits + + # ── C# ─────────────────────────────────────────────────────────────────────── def test_csharp_no_error():