From 1edf4259f108fc14e6d7ac74aa2e7ad09fa436af Mon Sep 17 00:00:00 2001 From: ayushcodes10 Date: Mon, 7 Sep 2026 13:05:09 +0530 Subject: [PATCH] Extract Rust trait methods A trait's declaration_list was never walked, so a method it declared had no node at all unless some impl in the same file happened to define a same-named method, in which case that impl's node stood in for it, anchored at the impl's line rather than the declaration's. A trait with no implementor anywhere in the corpus contributed zero method nodes. Two shapes were involved. A default-bodied method (function_item, the same node type an impl method already uses) now walks through the same member handling impl_item already had, via parent_impl_nid=item_nid. A signature-only method (function_signature_item, the grammar's node type for a bodyless fn foo(&self);) had no extraction path at all, so this adds a dedicated branch mirroring function_item's node/edge/parameter and return type handling minus the body it never has. Fixes #3366. --- graphify/extractors/rust.py | 35 +++++++++++++++++++++++ tests/test_multilang.py | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/graphify/extractors/rust.py b/graphify/extractors/rust.py index b663bd6250..54b2d6ea09 100644 --- a/graphify/extractors/rust.py +++ b/graphify/extractors/rust.py @@ -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: @@ -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": diff --git a/tests/test_multilang.py b/tests/test_multilang.py index 8701f2bff6..9577c1a20a 100644 --- a/tests/test_multilang.py +++ b/tests/test_multilang.py @@ -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():