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.

(cherry picked from commit 4300054291)
This commit is contained in:
rajatnagda45 authored and safishamsi committed 2026-09-20 13:59:25 +01:00
1 parent a9450a537e
commit fc8e9e167f
2 files changed
+106 -2

No files matched your search

+6 -2
View File
@@ -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 @@ _SWIFT_CONFIG = LanguageConfig(
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,
)
+100
View File
@@ -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()