From 430005429102ebd19dfb7fbba752af2f40738286 Mon Sep 17 00:00:00 2001 From: rajatnagda45 Date: Sat, 19 Sep 2026 19:42:39 +0530 Subject: [PATCH] fix(swift): extract method requirements declared in a protocol body tree-sitter-swift gives a protocol's body-less method requirement its own node type, protocol_function_declaration, instead of reusing the function_declaration used inside a class or struct. The Swift config's function_types only listed function_declaration, so a protocol like protocol Drawable { func draw() func area() -> Double } became an empty node - the method contract every conformer must implement never entered the graph. Add protocol_function_declaration to function_types and function_boundary_types. Requirements now surface as .method() nodes hung off the protocol, their signature type references (return/param types) are captured, and they stay distinct from a conformer's implementation of the same name. Stored property requirements are left alone, matching how stored properties on a class are already handled. Adds regression coverage for all three. --- graphify/extract.py | 8 +- tests/test_swift_protocol_requirements.py | 100 ++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 tests/test_swift_protocol_requirements.py diff --git a/graphify/extract.py b/graphify/extract.py index 77e0f3b724..da66f41317 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -1249,7 +1249,11 @@ def _import_swift(node, source: bytes, file_nid: str, stem: str, edges: list, st _SWIFT_CONFIG = LanguageConfig( ts_module="tree_sitter_swift", class_types=frozenset({"class_declaration", "protocol_declaration"}), - function_types=frozenset({"function_declaration", "init_declaration", "deinit_declaration", "subscript_declaration"}), + # `protocol_function_declaration` is the body-less method requirement inside a + # `protocol { ... }`; tree-sitter-swift gives it its own node type rather than + # reusing `function_declaration`, so without it a protocol's method contract + # is dropped and the protocol becomes an empty node. + function_types=frozenset({"function_declaration", "protocol_function_declaration", "init_declaration", "deinit_declaration", "subscript_declaration"}), import_types=frozenset({"import_declaration"}), call_types=frozenset({"call_expression"}), call_function_field="", @@ -1257,7 +1261,7 @@ def _import_swift(node, source: bytes, file_nid: str, stem: str, edges: list, st call_accessor_field="", name_fallback_child_types=("simple_identifier", "type_identifier", "user_type"), body_fallback_child_types=("class_body", "protocol_body", "function_body", "enum_class_body"), - function_boundary_types=frozenset({"function_declaration", "init_declaration", "deinit_declaration", "subscript_declaration"}), + function_boundary_types=frozenset({"function_declaration", "protocol_function_declaration", "init_declaration", "deinit_declaration", "subscript_declaration"}), import_handler=_import_swift, ) diff --git a/tests/test_swift_protocol_requirements.py b/tests/test_swift_protocol_requirements.py new file mode 100644 index 0000000000..ec41c28523 --- /dev/null +++ b/tests/test_swift_protocol_requirements.py @@ -0,0 +1,100 @@ +"""Regression coverage for method requirements declared in a Swift protocol. + +tree-sitter-swift gives a protocol's body-less method requirement its own node +type, ``protocol_function_declaration``, rather than reusing the +``function_declaration`` used inside a class/struct. The Swift config only +listed ``function_declaration``, so a protocol's method contract was dropped and +the protocol became an empty node -- the API surface every conformer must +implement never entered the graph. +""" +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from graphify.extract import extract_swift + + +def _labels(result): + return [n["label"] for n in result["nodes"]] + + +class TestSwiftProtocolRequirements(unittest.TestCase): + def _extract(self, src: str) -> dict: + with tempfile.TemporaryDirectory() as d: + p = Path(d) / "Proto.swift" + p.write_text(src, encoding="utf-8") + return extract_swift(p) + + def test_protocol_method_requirements_become_methods(self): + r = self._extract( + "protocol Drawable {\n" + " func draw()\n" + " func area() -> Double\n" + "}\n" + ) + proto_nid = next(n["id"] for n in r["nodes"] if n["label"] == "Drawable") + method_targets = { + n["label"] + for e in r["edges"] + if e["relation"] == "method" and e["source"] == proto_nid + for n in r["nodes"] + if n["id"] == e["target"] + } + self.assertEqual(method_targets, {".draw()", ".area()"}) + + def test_protocol_method_return_type_reference_is_captured(self): + # The requirement's body-less signature still carries a return type. + r = self._extract( + "protocol Sized {\n" + " func area() -> Double\n" + "}\n" + ) + area_nid = next(n["id"] for n in r["nodes"] if n["label"] == ".area()") + ref_targets = { + n["label"] + for e in r["edges"] + if e["relation"] == "references" and e["source"] == area_nid + for n in r["nodes"] + if n["id"] == e["target"] + } + self.assertIn("Double", ref_targets) + + def test_protocol_and_conformer_methods_are_distinct_nodes(self): + r = self._extract( + "protocol Drawable {\n" + " func draw()\n" + "}\n\n" + "struct Circle: Drawable {\n" + " func draw() {}\n" + "}\n" + ) + proto_nid = next(n["id"] for n in r["nodes"] if n["label"] == "Drawable") + circle_nid = next(n["id"] for n in r["nodes"] if n["label"] == "Circle") + proto_draw = { + e["target"] for e in r["edges"] + if e["relation"] == "method" and e["source"] == proto_nid + } + circle_draw = { + e["target"] for e in r["edges"] + if e["relation"] == "method" and e["source"] == circle_nid + } + self.assertTrue(proto_draw and circle_draw) + self.assertTrue( + proto_draw.isdisjoint(circle_draw), + "protocol requirement and conformer method collapsed onto one node", + ) + # The conformance heritage edge is untouched. + self.assertTrue( + any( + e["relation"] == "implements" + and e["source"] == circle_nid + and e["target"] == proto_nid + for e in r["edges"] + ) + ) + + +if __name__ == "__main__": + unittest.main()