From cd3a376030d0dcfdccdac05eac0f4d5c34308fb6 Mon Sep 17 00:00:00 2001
From: Synvoya <16019863+Synvoya@users.noreply.github.com>
Date: Thu, 2 Jul 2026 00:21:45 +1000
Subject: [PATCH] fix(objc): emit implements edge for protocol-to-protocol
adoption
`@protocol Derived ` dropped the protocol-adoption (inheritance)
edge. The protocol_declaration handler in extract_objc walked children
for method declarations but ignored the protocol_reference_list child
that holds the adopted protocols, so no implements edge was ever emitted
for protocol-on-protocol adoption.
The extractor already handled `@interface Foo ` adoption, but that
nests the protocol name under a parameterized_arguments node; protocol-on-
protocol adoption uses a different grammar node (protocol_reference_list)
whose adopted-name is a direct `identifier` child, so it was never
matched. Walk protocol_reference_list and emit an implements edge for each
adopted protocol, mirroring the @interface handling.
Adds a defined Base/Derived protocol pair to the ObjC fixture and a
regression test asserting the Derived->Base implements edge.
---
graphify/extract.py | 12 ++++++++++++
tests/fixtures/sample.m | 12 ++++++++++++
tests/test_languages.py | 10 ++++++++++
3 files changed, 34 insertions(+)
diff --git a/graphify/extract.py b/graphify/extract.py
index 19dcaa2..6887d08 100644
--- a/graphify/extract.py
+++ b/graphify/extract.py
@@ -11625,6 +11625,18 @@ def extract_objc(path: Path) -> dict:
proto_nid = _make_id(stem, name)
add_node(proto_nid, f"<{name}>", line)
add_edge(file_nid, proto_nid, "contains", line)
+ # Adopted protocols: `@protocol Derived `. These
+ # nest under a protocol_reference_list node (distinct from the
+ # parameterized_arguments node used by @interface adoption), so
+ # they were never emitted. Emit an `implements` edge for each,
+ # matching how @interface protocol adoption is handled.
+ for child in node.children:
+ if child.type == "protocol_reference_list":
+ for sub in child.children:
+ if sub.type == "identifier":
+ base_nid = ensure_named_node(_read(sub), line)
+ if base_nid != proto_nid:
+ add_edge(proto_nid, base_nid, "implements", line)
for child in node.children:
walk(child, proto_nid)
return
diff --git a/tests/fixtures/sample.m b/tests/fixtures/sample.m
index 2f1209a..4fd0f93 100644
--- a/tests/fixtures/sample.m
+++ b/tests/fixtures/sample.m
@@ -40,3 +40,15 @@
}
@end
+
+@protocol Base
+
+- (void)baseMethod;
+
+@end
+
+@protocol Derived
+
+- (void)derivedMethod;
+
+@end
diff --git a/tests/test_languages.py b/tests/test_languages.py
index 35569c5..8227131 100644
--- a/tests/test_languages.py
+++ b/tests/test_languages.py
@@ -1064,6 +1064,16 @@ def test_objc_splits_inherits_and_implements():
assert ("Animal", "SampleDelegate") in _edge_labels(r, "implements")
+def test_objc_protocol_adopts_protocol():
+ """`@protocol Derived ` must emit an implements edge Derived->Base.
+ Protocol-on-protocol adoption nests under a protocol_reference_list node
+ (distinct from the parameterized_arguments node used by @interface
+ adoption), so the edge was previously dropped. Protocol nodes are labeled
+ ``, so the edge reads (, )."""
+ r = extract_objc(FIXTURES / "sample.m")
+ assert ("", "") in _edge_labels(r, "implements")
+
+
def test_objc_property_type_context():
r = extract_objc(FIXTURES / "sample.m")
assert ("Animal", "NSString") in _edge_labels(r, "references", "field")