Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions graphify/extractors/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
16 changes: 16 additions & 0 deletions tests/test_languages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading