Skip to content
Open
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
35 changes: 35 additions & 0 deletions graphify/extractors/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,26 @@ def walk(node, parent_impl_nid: str | None = None) -> None:
function_bodies.append((func_nid, body))
return

if t == "function_signature_item":
# A bodyless method declaration (`fn foo(&self);`), the shape a
# trait method takes when it has no default implementation. Same
# name/parameters/return_type fields as function_item, just no
# body field to walk (#3366).
name_node = node.child_by_field_name("name")
if name_node:
func_name = _read_text(name_node, source)
line = node.start_point[0] + 1
if parent_impl_nid:
func_nid = _make_id(parent_impl_nid, func_name)
add_node(func_nid, f".{func_name}()", line)
add_edge(parent_impl_nid, func_nid, "method", line)
else:
func_nid = _make_id(stem, func_name)
add_node(func_nid, f"{func_name}()", line)
add_edge(file_nid, func_nid, "contains", line)
emit_param_return_refs(node, func_nid, line)
return

if t in ("struct_item", "enum_item", "trait_item"):
name_node = node.child_by_field_name("name")
if name_node:
Expand Down Expand Up @@ -209,6 +229,21 @@ def walk(node, parent_impl_nid: str | None = None) -> None:
else:
add_edge(item_nid, tgt, "references", line,
context="generic_arg")
# A trait's own declaration_list was never walked, so every
# method it declares was invisible: a default-bodied one
# (function_item) only got a node if some impl in the same
# file happened to define a same-named method, anchored at
# the impl's line rather than the trait's; a signature-only
# one (function_signature_item, the grammar's node type for
# a bodyless `fn foo(&self);`) had no extraction path at
# all (#3366). Walking with parent_impl_nid=item_nid routes
# a default-bodied method through the same member-call
# handling impl_item already uses for its own methods.
for c in node.children:
if c.type != "declaration_list":
continue
for member in c.children:
walk(member, parent_impl_nid=item_nid)
if t == "struct_item":
for c in node.children:
if c.type != "field_declaration_list":
Expand Down
56 changes: 56 additions & 0 deletions tests/test_multilang.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,62 @@ def test_rust_no_cross_crate_spurious_edges():
)


def test_rust_trait_signature_only_method_gets_a_node():
"""#3366: a bodyless trait method (`fn run(&self);`) is a
function_signature_item, a node type the extractor never handled at all, so
it had no extraction path -- not even a mangled one. Processor.run in
sample.rs is exactly this shape. DataProcessor's own `impl Processor`
override also displays as `.run()`, so this locates the trait's own node
by id prefix (via the method edge below) rather than by label, since the
two are otherwise indistinguishable by label alone."""
r = extract_rust(FIXTURES / "sample.rs")
processor_id = next(n["id"] for n in r["nodes"] if n["label"] == "Processor")
method_edges = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "method"}
run_id = next((tgt for src, tgt in method_edges if src == processor_id), None)
assert run_id is not None, "Processor -> .run() method edge missing"
run_node = next(n for n in r["nodes"] if n["id"] == run_id)
assert run_node["label"] == ".run()"
assert run_node["source_location"] == "L30", (
f"anchored at the wrong line (impl's L46 instead of the declaration's L30): "
f"{run_node['source_location']}"
)


def test_rust_trait_method_with_no_impl_anywhere_in_the_corpus():
"""#3366: a trait's method used to contribute a node only as a side effect
of some impl in the same file happening to define a same-named method --
a trait with no implementor at all got zero method nodes. Logger has no
impl anywhere in sample.rs (only Processor is implemented, by
DataProcessor), so this exercises that with no synthetic fixture."""
r = extract_rust(FIXTURES / "sample.rs")
labels = {n["label"]: n["id"] for n in r["nodes"]}
assert ".log()" in labels, f"got {sorted(labels)}"
log_id = labels[".log()"]
logger_id = next(n["id"] for n in r["nodes"] if n["label"] == "Logger")
method_edges = {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == "method"}
assert (logger_id, log_id) in method_edges


def test_rust_trait_default_bodied_method_gets_a_node(tmp_path):
"""#3366: sample.rs has no default-bodied trait method (function_item
inside a trait's declaration_list, as opposed to the bodyless
function_signature_item the other tests here cover), so this is the only
coverage for that shape -- it was dropped just as completely as the
signature-only case, including when nothing in the corpus overrides it."""
p = tmp_path / "lonely.rs"
p.write_text(
"pub trait Lonely {\n"
" fn only_signature(&self) -> u8;\n"
" fn with_default(&self) -> u8 { 42 }\n"
"}\n",
encoding="utf-8",
)
r = extract_rust(p)
labels = [n["label"] for n in r["nodes"]]
assert ".only_signature()" in labels, f"got {labels}"
assert ".with_default()" in labels, f"got {labels}"


# ── extract() dispatch ────────────────────────────────────────────────────────

def test_extract_dispatches_all_languages():
Expand Down
Loading