mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 23:49:14 +00:00
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:
1 parent
a9450a537e
commit
fc8e9e167f
2 files changed
+106
-2
No files matched your search
+6
-2
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user