From e9fbe2250343a4fc36279c191b92c416bf0fdfe2 Mon Sep 17 00:00:00 2001 From: Santosh Purbey Date: Sun, 6 Sep 2026 08:33:31 +0545 Subject: [PATCH] fix(rust): extract trait method declarations (#3366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A trait got a node; the methods it declares did not. Across three real Rust repositories 504 of 654 trait methods (77%) had no node at all, so a trait — the API contract — appeared in the graph as a bare name with no operations attached, and "what must an implementor provide?" was unanswerable. Two gaps, both here: - `function_signature_item`, the node type for a bodyless `fn greet(&self);`, had no branch anywhere in the package, so a signature-only method had no extraction path. - the struct/enum/trait branch returns before descending, so nothing walked a trait's body — a default-bodied method inside a trait was never reached either. The methods that did appear came from impl blocks that happened to define the same name, and were anchored at the impl rather than the declaration. This adds a `function_signature_item` branch mirroring `function_item` (minus the body walk, since there is none) and descends into the trait body the way `impl_item` already descends into its own, attributing each method to the trait node with a `method` edge. Before / after on the reproducer in #3366: src/lonely.rs (a trait with no implementor in the corpus) before: Lonely after: Lonely, .only_signature() L3, .with_default() L4 + method edges from Lonely On a 937-file Rust service: 112 trait methods declared, 70 missing -> 0 missing; 9426 -> 9574 nodes, 28638 -> 29177 edges. Full suite is unchanged: 17 failed / 5296 passed / 93 skipped both with and without this commit (the 17 are pre-existing on v8, all in test_skillgen.py). Note for anyone verifying: the AST cache is keyed on file content, not extractor version, so `graphify-out/cache` must be removed to observe the change — `--force` alone re-runs extraction but still serves cached per-file results. --- graphify/extractors/rust.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/graphify/extractors/rust.py b/graphify/extractors/rust.py index b663bd625..72cf95619 100644 --- a/graphify/extractors/rust.py +++ b/graphify/extractors/rust.py @@ -182,6 +182,27 @@ def extract_rust(path: Path) -> dict: function_bodies.append((func_nid, body)) return + if t == "function_signature_item": + # `fn greet(&self) -> String;` — a trait method with no body. This node type + # had no branch at all, so a trait's required methods were unreachable: the + # only method nodes in a graph came from impl blocks, and a trait with no + # implementor in the corpus contributed none. Mirrors function_item, minus + # the body walk (there is no body). + 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: @@ -292,6 +313,16 @@ def extract_rust(path: Path) -> dict: continue type_node = field.child_by_field_name("type") _emit_enum_type(type_node, field.start_point[0] + 1) + if t == "trait_item": + # The methods a trait declares are its contract, and they were not + # reached: this branch returns below, so nothing walked the body. + # Descend the same way impl_item does, attributing each method to + # the trait node — so `explain ` can list what an + # implementor must provide. + body = node.child_by_field_name("body") + if body: + for child in body.children: + walk(child, parent_impl_nid=item_nid) return if t == "impl_item":