mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-25 08:11:54 +00:00
fix(swift): extract macro declarations and resolve freestanding macro calls
Swift 5.9 macros were dropped entirely. `macro stringify(...) = #externalMacro(...)` parses as a `macro_declaration` (freestanding or attached), which was in neither `function_types` nor `class_types`, so the symbol never entered the graph. A freestanding call `#stringify(...)` parses as a `macro_invocation`, not a `call_expression`, so its call edge was lost too. - Add `macro_declaration` to the Swift `function_types`. It has no `name` field, but the name is a bare `simple_identifier` child already covered by `name_fallback_child_types`, so the existing name resolution handles it; the generic `<T>` type parameter is excluded by the existing type-param scope pass (no `t` god node). - Add `macro_invocation` to `call_types` and give it a callee branch in walk_calls: a freestanding macro is a free top-level symbol (resolved by bare name, no `self` receiver). Calls inside the macro's arguments (`#stringify(helper())`) still resolve because walk_calls recurses into the invocation's children. Attached-macro *usage* (`@AddCompletion` on a type) is an attribute, not a `macro_invocation`, so it is not linked to the macro — left unresolved (fail-closed); the declaration itself is still extracted.
This commit is contained in:
1 parent
0a351eaa37
commit
f893a6e2da
3 files changed
+98
-3
No files matched your search
+8
-2
@@ -1715,9 +1715,15 @@ _SWIFT_CONFIG = LanguageConfig(
|
||||
# `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"}),
|
||||
# `macro_declaration` is the Swift 5.9 `macro stringify(...) = #externalMacro(...)`
|
||||
# form (freestanding or attached). It has no `name` field — the name is a bare
|
||||
# `simple_identifier` child, already covered by `name_fallback_child_types` — so
|
||||
# without it the whole declaration (and its `#name` call sites) was dropped.
|
||||
function_types=frozenset({"function_declaration", "protocol_function_declaration", "init_declaration", "deinit_declaration", "subscript_declaration", "macro_declaration"}),
|
||||
import_types=frozenset({"import_declaration"}),
|
||||
call_types=frozenset({"call_expression"}),
|
||||
# `macro_invocation` is a freestanding macro call `#stringify(...)`; it is not a
|
||||
# `call_expression`, so it needs its own callee branch in walk_calls.
|
||||
call_types=frozenset({"call_expression", "macro_invocation"}),
|
||||
call_function_field="",
|
||||
call_accessor_node_types=frozenset({"navigation_expression"}),
|
||||
call_accessor_field="",
|
||||
|
||||
@@ -8765,9 +8765,18 @@ def _extract_generic(
|
||||
callee_name = _read_text(node, source)
|
||||
self_receiver = "self"
|
||||
elif config.ts_module == "tree_sitter_swift":
|
||||
# Freestanding macro call `#stringify(...)`: `macro_invocation` is
|
||||
# `# simple_identifier call_suffix`. The macro is a free top-level
|
||||
# symbol, not a method on `self`, so resolve it by bare name without
|
||||
# a receiver (an unresolved `#name` stays unresolved — fail-closed).
|
||||
if node.type == "macro_invocation":
|
||||
for child in node.children:
|
||||
if child.type == "simple_identifier":
|
||||
callee_name = _read_text(child, source)
|
||||
break
|
||||
# Swift: first child may be simple_identifier or navigation_expression
|
||||
first = node.children[0] if node.children else None
|
||||
if first:
|
||||
if node.type != "macro_invocation" and first:
|
||||
if first.type == "simple_identifier":
|
||||
callee_name = _read_text(first, source)
|
||||
# Inside a method a bare `save()` is `self.save()`.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Swift 5.9 macro declarations must be extracted, not silently dropped.
|
||||
|
||||
`macro stringify(...) = #externalMacro(...)` parses as a `macro_declaration`,
|
||||
and a freestanding call `#stringify(...)` parses as a `macro_invocation` —
|
||||
neither of which is a `function_declaration`/`call_expression`. Before the fix
|
||||
the whole macro declaration (and every `#name` call site) was dropped: the
|
||||
symbol never appeared in the graph. A `macro_declaration` has no `name` field
|
||||
(the name is a bare `simple_identifier`, covered by Swift's
|
||||
`name_fallback_child_types`).
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
|
||||
def _labels(r):
|
||||
return [n["label"] for n in r["nodes"]]
|
||||
|
||||
|
||||
def _calls(r):
|
||||
id_to_label = {n["id"]: n["label"] for n in r["nodes"]}
|
||||
return {
|
||||
(id_to_label.get(e["source"]), id_to_label.get(e["target"]))
|
||||
for e in r["edges"]
|
||||
if e["relation"] == "calls"
|
||||
}
|
||||
|
||||
|
||||
def test_freestanding_macro_declaration_is_extracted(tmp_path):
|
||||
(tmp_path / "m.swift").write_text(
|
||||
'@freestanding(expression)\n'
|
||||
'macro stringify<T>(_ value: T) -> (T, String) = '
|
||||
'#externalMacro(module: "M", type: "S")\n'
|
||||
)
|
||||
r = extract([tmp_path / "m.swift"], cache_root=tmp_path, parallel=False)
|
||||
assert any("stringify" in l for l in _labels(r)), (
|
||||
"macro declaration was dropped"
|
||||
)
|
||||
# The generic type parameter `T` must not leak as a sourceless god node.
|
||||
assert not any(e.get("target") == "t" for e in r["edges"])
|
||||
|
||||
|
||||
def test_attached_macro_declaration_is_extracted(tmp_path):
|
||||
(tmp_path / "m.swift").write_text(
|
||||
'@attached(member)\n'
|
||||
'macro AddCompletion() = #externalMacro(module: "M", type: "AddCompletionMacro")\n'
|
||||
)
|
||||
r = extract([tmp_path / "m.swift"], cache_root=tmp_path, parallel=False)
|
||||
assert any("AddCompletion" in l for l in _labels(r))
|
||||
|
||||
|
||||
def test_freestanding_macro_invocation_resolves_to_declaration(tmp_path):
|
||||
(tmp_path / "m.swift").write_text(
|
||||
'macro stringify<T>(_ value: T) -> (T, String) = '
|
||||
'#externalMacro(module: "M", type: "S")\n'
|
||||
'\n'
|
||||
'func use() {\n'
|
||||
' let x = #stringify(1 + 1)\n'
|
||||
'}\n'
|
||||
)
|
||||
r = extract([tmp_path / "m.swift"], cache_root=tmp_path, parallel=False)
|
||||
assert ("use()", "stringify()") in _calls(r), (
|
||||
"freestanding macro call #stringify did not resolve to the macro"
|
||||
)
|
||||
|
||||
|
||||
def test_call_inside_macro_argument_still_resolves(tmp_path):
|
||||
# Recursing into a macro_invocation's arguments must still find ordinary
|
||||
# calls: `#stringify(helper())` keeps `use() -> helper()`.
|
||||
(tmp_path / "m.swift").write_text(
|
||||
'macro stringify<T>(_ value: T) -> (T, String) = '
|
||||
'#externalMacro(module: "M", type: "S")\n'
|
||||
'\n'
|
||||
'func helper() -> Int { return 1 }\n'
|
||||
'func use() { let x = #stringify(helper()) }\n'
|
||||
)
|
||||
r = extract([tmp_path / "m.swift"], cache_root=tmp_path, parallel=False)
|
||||
calls = _calls(r)
|
||||
assert ("use()", "stringify()") in calls
|
||||
assert ("use()", "helper()") in calls
|
||||
Reference in new issue
Block a user