Skip to content

fix(resolve): keep Rust method calls that lost their receiver off functions and std names - #307

Merged
sunerpy merged 2 commits into
mainfrom
fix/rust-receiver-gates
Oct 3, 2026
Merged

sunerpy merged 2 commits into
mainfrom
fix/rust-receiver-gates

Conversation

@sunerpy

@sunerpy sunerpy commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

join, iter, as_str and is_empty topped this repository's most-called lists because of false edges. The extractor keeps a Rust receiver only for:

  • an identifier;
  • self;
  • one self.<field> hop;
  • a path::call() receiver.

Every other receiver leaves the bare method name. That covers a plain call root(), a deeper field chain, vec![…], ?, .await, and a chain broken across lines; upstream's tree-sitter.ts does the same.

Resolution then treated root().join(name) as a bare call, and the project's only join method won at exact-match 0.9. That method is LineIndex::join, which 169 files "called" 964 times.

Upstream has no Rust counterpart to its JS/TS gates (#1987, #1714/#1857, #1683), so these are KEEP-RUST rules, stricter than upstream. The design passed the kirocodex plan gate in round 4 (6 → 1 → 1 → 0); the plan is /config/.claude/plans/p15-rust-receiver-gates.md.

The rules

Shape. It is read from the source line. A ref's column is the call expression's start, so:

  • a bare call starts with its name;
  • a dropped receiver starts with something else, and its .name( follows within 4 KiB;
  • an unknown shape (no source, or a column off its line) keeps today's behaviour.
rule a Rust calls ref binds
R1 with neither receiver nor path (helper()) never a method
R2 whose receiver was dropped (root().join(x)) only a method, never through use (decided before import resolution), never by a fuzzy guess
R3 whose receiver was dropped, named in RUST_STD_METHODS (307 std names, sorted, pinned) nothing
R4 bare Ok / Err / Some / None a project variant only when a use naming it, or a glob, is in its own block and module

R4's names are the ones a use brings in. A leaf introduces its alias, else its last path segment, and a self leaf the segment before its group. So use …::Err as Failure brings in Failure, and a later bare Err(..) is still the prelude's; as _ brings in nothing. This came from review round 1 (second commit, ec1bf0f).

R4's scope is Rust's own. A use is visible in its block and that block's nested blocks. An inline mod NAME { … } is a boundary, crossed only by the child's own use super::…. The scope is computed once per file from masked source (SourceFacts::rust_use_in_scope). If the braces do not balance, every use counts file-wide, which can only keep today's binding.

rust_code_mask moves unchanged from frameworks/tauri.rs into a shared rust_lexical module; Tauri's 59 tests pass as they were. Extraction version 20 → 21.

Tests

Red first. On main, the three negative integration tests fail for the stated reason (/tmp/evidence-p15/red-tier2.log):

  • helper() binds Store::helper;
  • dropped_join binds LineIndex::join;
  • a child module's Err(2) binds Outcome::Err.

The shape precondition and the positives pass on main too.

Integration fixture. tests/fixtures/tier2/rust_receiver/ holds five tests in tier2_false_edges.rs, run through the full extract → resolve pipeline:

  • It first asserts the bare join / take shape the extractor gives root().join(x), a broken chain, and make_opt().take().

  • None of those binds LineIndex::join / Cache::take, nor the imported free fn join.

  • These still bind:

    • self.join(..);
    • LineIndex::join(..);
    • store().nodes_by_ids(..).
  • A bare helper() and the imported join(..) bind their free fns, and only_method() binds nothing.

  • Err / Ok bind Outcome::* at:

    • a top-level use;
    • a child use super::*;
    • a braced use;
    • a fn-local use.

    But not:

    • in a child module of a top-level use;
    • in a sibling fn;
    • behind a commented-out use;
    • after use …::Err as Failure.

Unit tests.

  • rust_call_shape: bare, turbofish, dropped receiver, broken chain, joined vs join, a range end, off-line columns, non-Rust and non-calls refs.
  • Each rule on match_by_exact_name, including "no source keeps today's binding".
  • Fuzzy matching closed for both shapes.
  • RUST_STD_METHODS sorted.
  • RustUseScopes: blocks, modules, a comment, use<>, a braced tree past a comment, unbalanced braces, and the names a use tree introduces (aliases, as _, braced groups, self leaves).

Verification

  • make pre-ci passed on the head ec1bf0f: 4360 Rust tests, 0 failed; 569 frontend tests; bundle byte check; archive smoke (/tmp/evidence-p15/pre-ci-r2.log). The goldens, the rule diff, the sync check and the 0.53.1 acceptance dry run below were re-run with a release binary built from that head (sha256 132a1621…; the *-r2.log files). The numbers did not change: this repository imports no prelude-named variant under an alias.
  • Goldens. Every corpus was re-extracted with the new binary, following docs/equivalence.md's recipe; all 19 are identical (golden-drift.log). No corpus has a Rust call resolved by exact-match or fuzzy.
  • This repository. An archive of 6cc9507 was indexed with the 0.53.0 and new binaries (diff_rules.py, removed-samples.txt):
    • 3,284 edges removed, each attributed to one rule: R3 2,743, R1 296, R4 193, R2 52.
    • 20 sampled per rule, all 80 false: drop(x) → a Drop impl, .context() → an import, .iter() → OrderedMap::iter, Err(…) → Dispatch::Err.
    • 48 edges added where a filtered candidate gave way. In 24 of them a bare node(…) reaches its free fn instead of a method. In 13, .extract(…) reaches FrameworkResolver::extract, now under the ambiguity ceiling. One new false edge: .id() → a fixture's id.
    • 3 edges keep their target with a lower confidence. On index_root.join("diagnostics").join(&filename) the outer, dropped-receiver call is gone; the inner index_root.join still binds through the member path's word overlap ("index"), which this change leaves alone.
  • sync == index --force. After moving LineIndex::join, nodes, edges and unresolved refs are identical (sync-equiv.log).
  • Timing. A full index of the archive, glibc builds of main vs this change, three runs each: 7.80 / 7.71 / 7.64 s vs 7.66 / 7.63 / 7.51 s (timing.log).

Known limits (from the plan)

  • R3 drops a true edge when a project method shares a std name and is called through a dropped receiver (self.store().get(id)); this is #1987's trade-off.
  • Third-party crate methods (is_match, body, json) and std types outside the list (SocketAddr::port) are not gated: 451 Method-shaped edges survive, and a sample of 20 had about six such cases.
  • Member-path refs (index_root.join) still bind std names through word overlap. A #1987-style gate there would be a follow-up.

🤖 Generated with Claude Code

CodeGraph Test added 2 commits October 3, 2026 14:03
…ctions and std names

The extractor keeps a Rust receiver only for an identifier, `self`, one
`self.<field>` hop and a `path::call()` receiver. Any other receiver (a plain
call, a deeper field chain, `vec![…]`, `?`, `.await`, a chain broken across
lines) leaves the bare method name, so `root().join(name)` resolved like a bare
call and bound the project's only `join` method at 0.9. In this repository,
964 calls from 169 files landed on `LineIndex::join`.

Resolution now reads such a call's shape from its source line, where the ref's
column is the call expression's start. These KEEP-RUST rules are stricter than
upstream:

- R1: a call with neither receiver nor path never binds a method;
- R2: a call whose receiver was dropped binds only a method, never through a
  `use` binding and never by a fuzzy guess;
- R3: such a call binds nothing when its name is one of 307 std methods;
- R4: a bare `Ok`/`Err`/`Some`/`None` binds a project variant only when a `use`
  naming it, or a glob, is in its own block and module.

`rust_code_mask` moves from the Tauri resolver into a shared `rust_lexical`
module, which also records where each `use` item is visible.

Extraction version 21. On this repository the four rules remove 3,284 edges
(R3 2,743, R1 296, R4 193, R2 52). Every sampled one is false, and no golden
moves.
`RustUseScopes` recorded every identifier of a `use` item, so
`use crate::outcome::Outcome::Err as Failure;` counted as importing `Err`, and a
later bare `Err(..)`, which is the prelude's, kept its false binding to the
project variant. A use tree now contributes:

- each leaf's alias (`as _` contributes nothing);
- otherwise the leaf's last path segment;
- for a `self` leaf, the segment before its group.

The tier-2 fixture gains the aliased case, and a unit test covers aliases,
braced groups, `self` leaves and `as _`.
@codecov

codecov Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.31547% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/codegraph-resolve/src/name_matcher.rs 97.74% 6 Missing ⚠️
crates/codegraph-resolve/src/rust_lexical.rs 98.64% 5 Missing ⚠️

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #307      +/-   ##
==========================================
+ Coverage   95.06%   95.07%   +0.01%     
==========================================
  Files         197      198       +1     
  Lines      108505   108998     +493     
==========================================
+ Hits       103147   103634     +487     
- Misses       5358     5364       +6     
Files with missing lines Coverage Δ
crates/codegraph-resolve/src/frameworks/tauri.rs 92.64% <ø> (-0.54%) ⬇️
crates/codegraph-resolve/src/resolver.rs 97.10% <100.00%> (+<0.01%) ⬆️
crates/codegraph-resolve/src/source_facts.rs 100.00% <100.00%> (ø)
crates/codegraph-store/src/index_state.rs 83.33% <ø> (ø)
crates/codegraph-resolve/src/rust_lexical.rs 98.64% <98.64%> (ø)
crates/codegraph-resolve/src/name_matcher.rs 96.80% <97.74%> (+0.03%) ⬆️

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sunerpy
sunerpy merged commit f9f96c3 into main Oct 3, 2026
11 checks passed
@sunerpy
sunerpy deleted the fix/rust-receiver-gates branch October 3, 2026 06:41
@github-actions github-actions Bot mentioned this pull request Oct 3, 2026
sunerpy added a commit that referenced this pull request Oct 3, 2026
The release record for #307: the release PR merge and its tag SHA, the workflow
run, the published digest, and the black-box acceptance against v0.53.0.
Current alignment now lists v0.53.1 in place of the unreleased note.

Co-authored-by: CodeGraph Test <codegraph@example.invalid>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant