fix(rust): extract trait method declarations (#3366)

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.
This commit is contained in:
Santosh Purbey committed 2026-09-06 08:33:31 +05:45
1 parent c9f9901877
commit e9fbe22503
1 file changed
+31
+31
View File
@@ -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 <Trait>` 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":