diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 90c95cc49..5d6d32019 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -2714,6 +2714,18 @@ def _emit_java_parent_type(type_node, rel: str, at_line: int) -> None: for c in extend.children: if c.type == "type_identifier": bases.append((_read_text(c, source), c.start_point[0] + 1)) + elif c.type == "stable_type_identifier": + # qualified base like `pkg.Base` / `a.b.Trait`: the + # segments are flat identifier children, so the real + # type name is the tail (type_)identifier, not the + # package path. Without this branch qualified + # extends/with clauses were skipped entirely. + tail = None + for sc in c.children: + if sc.type in ("type_identifier", "identifier"): + tail = sc + if tail is not None: + bases.append((_read_text(tail, source), c.start_point[0] + 1)) elif c.type == "generic_type": base = c.child_by_field_name("type") if base is None: diff --git a/tests/test_languages.py b/tests/test_languages.py index f610fbc40..ad2c80fd5 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -691,6 +691,19 @@ def test_scala_splits_inherits_and_mixes_in(): assert ("HttpClient", "Loggable") in _edge_labels(r, "mixes_in") +def test_scala_qualified_extends_uses_tail_name(tmp_path): + """A qualified base like `pkg.Base` is a `stable_type_identifier` node in the + extends_clause. The handler only matched `type_identifier`/`generic_type`, so + qualified extends/with clauses were dropped. Resolve them to the tail type + name (`Base`, `Trait1`), not the package path. + """ + f = tmp_path / "foo.scala" + f.write_text("class Foo extends pkg.Base with other.Trait1\n") + r = extract_scala(f) + assert ("Foo", "Base") in _edge_labels(r, "inherits") + assert ("Foo", "Trait1") in _edge_labels(r, "mixes_in") + + def test_scala_constructor_parameter_field_context(): r = extract_scala(FIXTURES / "sample.scala") assert ("HttpClient", "Config") in _edge_labels(r, "references", "field")