fix(rust): extract trait method declarations (fixes #3366) - #3367
fix(rust): extract trait method declarations (fixes #3366)#3367santoshpy wants to merge 1 commit into
Conversation
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 Graphify-Labs#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.
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Extracts trait method declarations that previously never made it into the graph: handles function_signature_item (bodyless trait methods like fn greet(&self) -> String;) by emitting a method node with param/return type refs, attributed to the enclosing impl or file, and descends into trait_item bodies so each declared method is attached to the trait node. This makes a trait's required methods visible to explain <Trait> even when no implementor exists in the corpus.
No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 27 functions depend on the 13 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract_rust()— 16 callers, 6 callees - new:
walk()— 1 callers, 8 callees
Verification — 27 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 27 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify extract\_rust.
The verifier did not have enough to check extract\_rust, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 2 more finding(s) on lines outside this diff (see the check run).
Fixes #3366.
The problem
A Rust trait gets a node; the methods it declares do not. Across three real Rust repositories, 504 of 654 trait method declarations (77%) have no node. A trait is the API contract, so it appears in the graph as a bare name with no operations attached —
explain <Trait>cannot say what an implementor must provide.The methods that do appear are only there because an
implblock in the same file happens to define a method of the same name, and they are anchored at the impl, not the declaration.Two gaps, both in
extractors/rust.pyfunction_signature_itemhad no branch anywhere in the package. That is the tree-sitter-rust node type for a bodylessfn greet(&self) -> String;, so a signature-only method had no extraction path at all.struct_item/enum_item/trait_itembranch returns before descending, so nothing walked a trait's body — a method with a default body inside a trait was never reached either.The change
function_signature_itembranch mirroringfunction_item, minus the body walk (there is no body).trait_itemcase, descend into the trait's body the wayimpl_itemalready descends into its own, passing the trait node asparent_impl_nidso each method gets amethodedge from the trait.Both follow the existing shape of the file rather than introducing a new mechanism.
Before / after
The reproducer from #3366 — a trait with no implementor in the corpus:
And where an impl exists, the declaration and the implementation are now distinct nodes (
Greeter::greetat the trait's line,Alice::greetat the impl's), instead of only the latter.Measured on a real codebase
A 937-file Rust service:
Tests
Full suite unchanged: 17 failed / 5296 passed / 93 skipped, identical with and without this commit. The 17 failures are pre-existing on
v8and all intest_skillgen.py; I verified by stashing the change and re-running.One note for reviewers verifying locally
The AST cache is keyed on file content, not extractor version, so
graphify-out/cachehas to be removed to observe any extractor change —--forcere-runs extraction but still serves cached per-file results. That cost me a confusing round of "the fix does nothing" before I spotted it; it may be worth folding the extractor version into the cache key separately.