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")