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).
Problem
Rust receiver-method calls (
self.method(),receiver.method()) never produce a cross-filecallsedge. Rust is the only major supported language with no_resolve_*_member_callspass,so its member calls are dropped by the shared guard and nothing recovers them.
On a real Rust codebase (doli-network/doli, 533
.rsfiles, ~24k-edge graph) this makes blast-radius queries silently wrong on exactly the symbols
that matter most — the hub methods on the main
impl:callsin graph.apply_block().rollback_one_block().calculate_epoch_rewards().execute_reorg().handle_new_block().add_transaction()Graph-wide: only 32% of the 2,306 method-labelled nodes have any incoming
callsedge.blast.py graph.json apply_block --hops 1returns 1 dependent for a function called from6 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 acall_expressionover afield_expression,so
extractors/rust.py:359tags itis_member_call=Trueand enqueues it intoraw_calls—it is not dropped at extraction.
It dies in every cross-file resolver:
symbol_resolution.py:265—if raw_call.get("is_member_call"): continuesymbol_resolution.py:336— sameextract.py:5198— same, with the rationale: "obj.log() → 'log' has no import evidence and collides with any top-level function named 'log' in the corpus" (Cross-file INFERREDcallsresolution lacks import evidence + ambiguous-candidate handling — short common names blow up god_nodes ranking #543/Cross-filecallsresolution drops EXTRACTED edges when the callee name has multiple definitions (even with unambiguous import evidence) #1219)That guard is correct on its own terms. The problem is that seven languages have a recovery pass
behind it and Rust has none:
Not covered by #2135
I built the graph twice on the same 533-file Rust tree — once on
v8(1644230, = 0.9.28) andonce on #2135 (
322a2a9),--no-cluster, cache cleared between runs: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 thiscodebase and I'd like to see it land.
But it moves the
is_scoped_callbranch (rust.py:364), and every hub method above is on theis_member_callbranch (rust.py:359). Measured: all six rows in the table are byte-identicalbefore 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 -> typetable, resolve the receiver's type, and emit only when the type name resolves toexactly 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 animpl Fooblock types the receiver asFoosyntactically — noinference needed, and this alone covers the whole table above.
let x = Foo::new();/let x: Foo = .../ typed fn params give a per-filename -> typetable for non-
selfreceivers._RUST_TRAIT_METHOD_BLOCKLISTand theone-survivor-or-bail rule.
Scoping just the
self.case insideimplblocks would be a small, high-confidence slice — itneeds no type inference at all, and it is where the missing edges concentrate.
Repro
Environment: graphify 0.9.28, Python 3.14, macOS 15 (arm64).