Skip to content

fix(rust): extract trait method declarations (fixes #3366) - #3367

Open
santoshpy wants to merge 1 commit into
Graphify-Labs:v8from
santoshpy:fix/rust-trait-methods
Open

fix(rust): extract trait method declarations (fixes #3366)#3367
santoshpy wants to merge 1 commit into
Graphify-Labs:v8from
santoshpy:fix/rust-trait-methods

Conversation

@santoshpy

Copy link
Copy Markdown
Contributor

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 impl block 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.py

  1. function_signature_item had no branch anywhere in the package. That is the tree-sitter-rust node type for a bodyless fn greet(&self) -> String;, so a signature-only method had no extraction path at all.
  2. The struct_item/enum_item/trait_item branch 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

  • Add a function_signature_item branch mirroring function_item, minus the body walk (there is no body).
  • In the trait_item case, descend into the trait's body the way impl_item already descends into its own, passing the trait node as parent_impl_nid so each method gets a method edge 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:

pub trait Lonely {
    fn only_signature(&self) -> u8;
    fn with_default(&self) -> u8 { 42 }
}
before:  L1 lonely.rs, L2 Lonely
after:   L1 lonely.rs, L2 Lonely, L3 .only_signature(), L4 .with_default()
         + method edges Lonely -> each

And where an impl exists, the declaration and the implementation are now distinct nodes (Greeter::greet at the trait's line, Alice::greet at the impl's), instead of only the latter.

Measured on a real codebase

A 937-file Rust service:

before after
trait methods with no node 70 / 112 (62%) 0 / 112
nodes 9,426 9,574
edges 28,638 29,177

Tests

Full suite unchanged: 17 failed / 5296 passed / 93 skipped, identical with and without this commit. The 17 failures are pre-existing on v8 and all in test_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/cache has to be removed to observe any extractor change — --force re-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.

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.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

safishamsi added a commit that referenced this pull request Sep 7, 2026
…ness (#3366)

Follow-up to #3367: guards that signature-only trait methods are captured and
that a trait-declared method stays a distinct node from its impl definition.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant