mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 07:29:33 +00:00
A `macro_rules! name { ... }` is a named, invocable item, but its node
type (macro_definition) had no branch, so the macro was dropped entirely:
it never became a node, and a `name!(...)` invocation of it (a
macro_invocation, never a call_expression) had nothing to resolve to.
Emit a node per macro_definition and resolve a bare `name!` invocation to
it with a calls edge. Macros are tracked in their own name registry, not
the function/type label map, because a macro and a function can share a
name (`vec!` vs `vec`); the node id is macro-qualified for the same
reason and the label carries a `!`. A scoped invocation (`log::info!`)
is cross-module/crate and stays unresolved (fail-closed).
Adds regression tests for the dropped definition, the resolved local
invocation, name coexistence with a same-named function, and the
unresolved scoped case.
(cherry picked from commit 7010322250)
94 lines
3.0 KiB
Python
94 lines
3.0 KiB
Python
"""Rust `macro_rules!` definitions and their invocations.
|
|
|
|
A `macro_rules! name { ... }` is a named, invocable item, but its node type
|
|
(`macro_definition`) had no branch in the extractor, so the macro was dropped
|
|
entirely: it never became a node and a `name!(...)` invocation of it (a
|
|
`macro_invocation`, never a `call_expression`) had nothing to resolve to.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
tsr = pytest.importorskip("tree_sitter_rust")
|
|
|
|
from graphify.extractors.rust import extract_rust
|
|
|
|
|
|
def _extract(tmp_path: Path, body: str) -> dict:
|
|
f = tmp_path / "lib.rs"
|
|
f.write_text(body, encoding="utf-8")
|
|
return extract_rust(f)
|
|
|
|
|
|
def test_macro_rules_definition_becomes_a_node(tmp_path):
|
|
r = _extract(
|
|
tmp_path,
|
|
"macro_rules! my_vec {\n"
|
|
" ($($x:expr),*) => { vec![$($x),*] };\n"
|
|
"}\n",
|
|
)
|
|
assert "error" not in r
|
|
labels = {n["label"] for n in r["nodes"]}
|
|
# Pre-fix the macro_definition was dropped: no node at all.
|
|
assert "my_vec!" in labels, f"macro_rules! definition dropped: {sorted(labels)}"
|
|
|
|
|
|
def test_macro_invocation_resolves_to_local_macro(tmp_path):
|
|
r = _extract(
|
|
tmp_path,
|
|
"macro_rules! my_vec {\n"
|
|
" ($($x:expr),*) => { vec![$($x),*] };\n"
|
|
"}\n"
|
|
"fn build() -> Vec<i32> {\n"
|
|
" my_vec!(1, 2, 3)\n"
|
|
"}\n",
|
|
)
|
|
id_to_label = {n["id"]: n["label"] for n in r["nodes"]}
|
|
calls = {
|
|
(id_to_label.get(e["source"], e["source"]),
|
|
id_to_label.get(e["target"], e["target"]))
|
|
for e in r["edges"] if e["relation"] == "calls"
|
|
}
|
|
assert ("build()", "my_vec!") in calls
|
|
|
|
|
|
def test_macro_does_not_collide_with_same_named_function(tmp_path):
|
|
"""A macro and a function may share a name (`vec!` vs `vec`). The macro id is
|
|
macro-qualified, so both coexist and the invocation binds to the macro only."""
|
|
r = _extract(
|
|
tmp_path,
|
|
"macro_rules! thing {\n"
|
|
" () => { 0 };\n"
|
|
"}\n"
|
|
"fn thing() -> i32 { 1 }\n"
|
|
"fn run() -> i32 {\n"
|
|
" thing!()\n"
|
|
"}\n",
|
|
)
|
|
labels = sorted(n["label"] for n in r["nodes"])
|
|
assert "thing!" in labels
|
|
assert "thing()" in labels
|
|
id_to_label = {n["id"]: n["label"] for n in r["nodes"]}
|
|
calls = {
|
|
(id_to_label.get(e["source"], e["source"]),
|
|
id_to_label.get(e["target"], e["target"]))
|
|
for e in r["edges"] if e["relation"] == "calls"
|
|
}
|
|
# The `thing!()` invocation binds to the macro, not the function.
|
|
assert ("run()", "thing!") in calls
|
|
assert ("run()", "thing()") not in calls
|
|
|
|
|
|
def test_scoped_macro_invocation_stays_unresolved(tmp_path):
|
|
"""A `log::info!(..)` invocation is cross-module/crate; it must not bind to any
|
|
local node (fail-closed), matching how scoped calls are handled."""
|
|
r = _extract(
|
|
tmp_path,
|
|
"fn run() {\n"
|
|
" log::info!(\"hi\");\n"
|
|
"}\n",
|
|
)
|
|
assert not [e for e in r["edges"] if e["relation"] == "calls"]
|