Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -1249,15 +1249,19 @@ 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="",
call_accessor_node_types=frozenset({"navigation_expression"}),
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,
)

Expand Down
100 changes: 100 additions & 0 deletions tests/test_swift_protocol_requirements.py
Original file line number Diff line number Diff line change
@@ -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()
Loading