Skip to content

fix(rust): receiver-method calls (self.method()) are dropped cross-file — Rust has no _resolve_*_member_calls pass #2234

Description

@isudoajl

Problem

Rust receiver-method calls (self.method(), receiver.method()) never produce a cross-file
calls edge. Rust is the only major supported language with no _resolve_*_member_calls pass,
so its member calls are dropped by the shared guard and nothing recovers them.

On a real Rust codebase (doli-network/doli, 533 .rs
files, ~24k-edge graph) this makes blast-radius queries silently wrong on exactly the symbols
that matter most — the hub methods on the main impl:

symbol incoming calls in graph real call sites (grep)
.apply_block() 0 68
.rollback_one_block() 0 22
.calculate_epoch_rewards() 0 18
.execute_reorg() 1 7
.handle_new_block() 0 5
.add_transaction() 26 76

Graph-wide: only 32% of the 2,306 method-labelled nodes have any incoming calls edge.

blast.py graph.json apply_block --hops 1 returns 1 dependent for a function called from
6 files, and it looks like a confident answer. That is worse than no graph — it is the failure
mode #2135's description names ("silently under-report, which pushes users back to grep"),
just on the other call form.

Where it is dropped

The extractor is fine: self.apply_block(...) is a call_expression over a field_expression,
so extractors/rust.py:359 tags it is_member_call=True and enqueues it into raw_calls
it is not dropped at extraction.

It dies in every cross-file resolver:

That guard is correct on its own terms. The problem is that seven languages have a recovery pass
behind it and Rust has none:

_resolve_swift_member_calls       (extract.py:2054, #1356)
_resolve_python_member_calls      (extract.py:2169)
_resolve_typescript_member_calls  (extract.py:2312)
_resolve_cpp_member_calls         (extract.py:2404)
_resolve_csharp_member_calls      (extract.py:2532)
_resolve_java_member_calls        (extract.py:2743)
_resolve_objc_member_calls        (extract.py:2832)
                                  ← no _resolve_rust_member_calls

Not covered by #2135

I built the graph twice on the same 533-file Rust tree — once on v8 (1644230, = 0.9.28) and
once on #2135 (322a2a9), --no-cluster, cache cleared between runs:

nodes:  9877 → 9877      (unchanged)
edges: 23920 → 24082     (+162, all EXTRACTED; 0 removed; INFERRED unchanged at 961)

The +162 are real — spot-checked at the exact source lines
(crate::gossip::staleness::classify_gossip(...), cmd_wallet::cmd_add_bls(...),
reward_epoch::is_epoch_start_with(...), run::run_node(...)). #2135 is a clean win on this
codebase and I'd like to see it land.

But it moves the is_scoped_call branch (rust.py:364), and every hub method above is on the
is_member_call branch (rust.py:359). Measured: all six rows in the table are byte-identical
before and after #2135.
Different guard, adjacent blind spot.

Suggested shape

The Swift pass (#1356) looks like the closest precedent: record the receiver plus a per-file
name -> type table, resolve the receiver's type, and emit only when the type name resolves to
exactly one definition — EXTRACTED when the type is explicit, INFERRED when locally inferred,
bail on ambiguity so the god-node guard is preserved.

For Rust the receiver-typing signal is comparatively strong:

  • self.method() inside an impl Foo block types the receiver as Foo syntactically — no
    inference needed, and this alone covers the whole table above.
  • let x = Foo::new(); / let x: Foo = ... / typed fn params give a per-file name -> type
    table for non-self receivers.
  • Trait-method ambiguity stays behind the existing _RUST_TRAIT_METHOD_BLOCKLIST and the
    one-survivor-or-bail rule.

Scoping just the self. case inside impl blocks would be a small, high-confidence slice — it
needs no type inference at all, and it is where the missing edges concentrate.

Repro

git clone https://github.com/doli-network/doli
cd doli && graphify update . --no-cluster
graphify explain ".apply_block()" --graph graphify-out/graph.json   # degree 6, zero incoming calls
grep -rn "\.apply_block(" crates bins --include="*.rs" | wc -l      # 68

Environment: graphify 0.9.28, Python 3.14, macOS 15 (arm64).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions