diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 90c95cc49..4f271ab26 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -608,22 +608,31 @@ def _php_method_return_type_node(method_node): return None def _kotlin_user_type_name(user_type_node, source: bytes) -> str | None: - """Return the head identifier text from a Kotlin user_type node (without generics).""" + """Return the tail identifier text from a Kotlin user_type node (without generics). + + A qualified supertype like `com.example.Base` lists its segments as flat + `identifier` children (`com`, `example`, `Base`) separated by `.` tokens, so + the real type is the LAST segment, not the first — returning the head yielded + the package root (`com`). Type arguments live in a separate `type_arguments` + child, so scanning direct children and keeping the last identifier/ + type_identifier segment ignores generics correctly (mirrors the C++ qualified + base handling, which uses the unqualified tail).""" if user_type_node is None: return None + name: str | None = None for c in user_type_node.children: - if c.type == "type_identifier": - text = _read_text(c, source) - return text or None - if c.type == "identifier": + if c.type in ("type_identifier", "identifier"): text = _read_text(c, source) - return text or None - if c.type == "simple_user_type": + if text: + name = text + elif c.type == "simple_user_type": for sub in c.children: if sub.type in ("identifier", "type_identifier"): text = _read_text(sub, source) - return text or None - return None + if text: + name = text + break + return name def _kotlin_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: """Walk a Kotlin type expression; append (name, role) tuples.""" diff --git a/tests/test_languages.py b/tests/test_languages.py index f610fbc40..8baacfa7f 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -649,6 +649,22 @@ def test_kotlin_interface_delegation_emits_implements(): assert ("LoggingList", "MutableList") in _edge_labels(r, "implements") +def test_kotlin_qualified_supertype_uses_tail_name(tmp_path): + """A qualified supertype like `com.example.Base` must resolve to the tail + type name (`Base`), not the package root (`com`). The Kotlin user_type lists + its segments as flat identifier children, and the walker returned the first + one, so every qualified base/interface collapsed onto a bogus `com` node. + """ + f = tmp_path / "foo.kt" + f.write_text("class Foo : com.example.Base(), com.example.Iface\n") + r = extract_kotlin(f) + assert ("Foo", "Base") in _edge_labels(r, "inherits") + assert ("Foo", "Iface") in _edge_labels(r, "implements") + # the package root must not leak in as a heritage target + assert ("Foo", "com") not in _edge_labels(r, "inherits") + assert ("Foo", "com") not in _edge_labels(r, "implements") + + def test_kotlin_parameter_return_generic_and_field_contexts(): r = extract_kotlin(FIXTURES / "sample.kt") assert ("run", "DataProcessor") in _edge_labels(r, "references", "parameter_type")