diff --git a/graphify/extract.py b/graphify/extract.py index f1cb548d6..db1cebaaa 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -3648,6 +3648,17 @@ def _php_emit_base(base_name: str, rel: str, at_line: int) -> None: if sub.type == "user_type": user_type_node = sub break + # `class Foo : Bar by baz` wraps the delegated + # interface `Bar` in an `explicit_delegation` + # node; grab its first `user_type` descendant so + # the implements edge (and generic-arg recovery) + # still fire. + if sub.type == "explicit_delegation": + for inner in sub.children: + if inner.type == "user_type": + user_type_node = inner + break + break if user_type_node is None: continue base = _kotlin_user_type_name(user_type_node, source) diff --git a/tests/fixtures/sample.kt b/tests/fixtures/sample.kt index 5c4489c5d..db3f2810d 100644 --- a/tests/fixtures/sample.kt +++ b/tests/fixtures/sample.kt @@ -35,6 +35,8 @@ class DataProcessor : BaseProcessor(), Loggable { override fun log() {} } +class LoggingList(inner: MutableList) : MutableList by inner + fun createClient(baseUrl: String): HttpClient { val config = Config(baseUrl, 30) return HttpClient(config) diff --git a/tests/test_languages.py b/tests/test_languages.py index ca5169d54..588b42a2f 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -623,6 +623,13 @@ def test_kotlin_splits_inherits_and_implements(): assert ("DataProcessor", "Loggable") in _edge_labels(r, "implements") +def test_kotlin_interface_delegation_emits_implements(): + """`class Foo : Bar by baz` wraps the delegated interface in an + `explicit_delegation` node — it must still emit an implements edge.""" + r = extract_kotlin(FIXTURES / "sample.kt") + assert ("LoggingList", "MutableList") 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")