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:
rajatnagda45 authored and safishamsi committed 2026-10-20 10:35:25 +01:00
1 parent 0a351eaa37
commit f893a6e2da
3 files changed
+98 -3

No files matched your search

+8 -2
View File
@@ -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="",
+10 -1
View File
@@ -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()`.
+80
View File
@@ -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