Fixes #1585. **Stacked on #1596** (the base branch is `fix/1588-rust-impl-type-qualification`; this PR's own diff is the second commit). Merge #1596 first, then retarget/merge this one. ## What was wrong ```rust impl Outer { pub fn run(&mut self) { self.inner.run(); // inner: Inner } } ``` produced `Outer::run -> Outer::run` — recursion the source doesn't contain. The extractor collapsed every `self.<field>.<method>()` receiver to the bare method name (`run`), so the resolver only ever saw `run` and exact-matched the nearest same-named method — the calling method itself, or a method of an unrelated type. Nothing marked the edge as a guess, and no row stayed in `unresolved_refs`, so a consumer had no way to tell. The same happened when the field's type isn't a project type at all (`its: std::vec::IntoIter<_>` → `self.its.next()`, `matcher: Regex` → `self.matcher.is_match()`): the bare `next` / `is_match` attached to whatever local method shared the name. ripgrep had 279 self-edges on `main`; the issue lists three sites, all of this shape. (The issue's C++ control — "`Outer::run -> Inner::run` resolves correctly" — doesn't actually hold on `main`: `inner.h` is classified as C by the `.h` heuristic, so `Inner::run` never exists and the C++ repro self-edges too. That's #1592, fixed separately.) ## What this does Rust struct fields are not graph nodes, so the field's type can only come from the struct's declaration text. This follows the Go 2-hop precedent exactly (`matchGoFieldChainCall`, #1276), including its exclusivity rule: 1. **Extraction (TS walker + native kernel, identical, parity-tested):** a call whose receiver is `self.<field>` keeps the owner-field shape — `self.inner.run()` is emitted as `self.inner.run`. Deeper chains (`self.a.b.m()`), call receivers (`self.f().m()`), parenthesized receivers and bare `self` keep the bare name, exactly as before. 2. **Resolution (`matchRustSelfFieldCall`):** owner type = the calling method's qualified-name prefix (`Outer::run` → `Outer`); the field's declared type is read from the owner struct's **own declaration lines** (comment-stripped, line by line — same discipline as the Go helper); the method is resolved **and validated** on that type by `resolveMethodOnType` (confidence 0.85, `instance-method`). 3. **Exclusive:** when the field is declared with an external type, a generic parameter (`T`), a container that doesn't auto-deref (`Option`/`Vec`/`Mutex`/…), or can't be found, the ref **stays unresolved** — it never falls through to the bare-name strategies. That is the safe behaviour the issue asks for, and it is what #1276 already chose for Go. `rustFieldTypeName` looks through exactly the layers Rust's method-call auto-deref looks through: references (`&`, `&'a mut`) and the owning smart pointers `Box`/`Rc`/`Arc`. `Box<dyn Source>` yields the trait, whose method node the interface-impl synthesizer then fans out to every implementation. `Option<Inner>` is left alone — `self.inner.take()` is Option's method and must not become `Inner::take`. Why it stacks on #1596: the owner is taken from the method's qualified name, which for a generic/lifetime impl was the trait's name before that fix. ## Measured on ripgrep (110 `.rs` files, #1596 build vs this branch) | | #1596 | this PR | |---|---|---| | nodes | 4029 | 4029 | | `calls` self-edges | 279 | **146** (none of the `self.<field>` shape remain — 116 bare-receiver, 30 other dotted) | | `self.<field>.m()` calls resolved through a validated field type | — | **292** (`DecompressionMatcher::command -> GlobSet::matches`, `Parser::find_long -> FlagMap::find`, `Haystack::path -> DirEntry::path`, …) | | `self.<field>.m()` calls left unresolved | — | **417** — every sampled one is a std/container method: `self.commands.push`, `self.child.wait`, `self.pre.is_some`, `self.colors.clone`, `self.path_terminator.unwrap_or` | | `calls` edges total | 9150 | 8878 (the 272 removed are the former bare-name guesses for those 417) | The issue's three sites: `walk.rs:824` now resolves to `IgnoreBuilder::add_custom_ignore_filename` (was a self-edge); `walk.rs:1195` (`self.its.next`, `IntoIter`) and `globset/lib.rs:983` (`self.matcher.is_match`, `Regex`) are parked as unresolved instead of guessed. The issue's repro gives `Outer::run -> Inner::run` (`instance-method`, confidence 0.85) on both the kernel path and `CODEGRAPH_KERNEL=0`. ## Tests - `__tests__/extraction.test.ts`: only the single-hop `self.<field>.<method>()` call keeps the prefix; deeper / call / parenthesized / bare-`self` receivers and a local receiver are unchanged. - `__tests__/resolution.test.ts` (end-to-end, Cargo layout): the issue's repro → `Outer::run -> Inner::run`, no self-edge; an external field type (`std::vec::IntoIter`) with a local `next` decoy → no edge at all; `Box<Inner>` and `&'a mut Inner` resolve, `Option<Inner>` does not (even though `Inner` declares the method); a generic `T` field → no edge; genuine `self.run()` recursion keeps its self-edge; the #1588 repro's `UsesFile::go` / `UsesBuf::go` resolve to `FileSource::read` / `BufSource::read`, and a `Box<dyn Source>` field lands on `Source::read` with the synthesizer fanning out to both impls. - `__tests__/fixtures/kernel-parity/torture.rs` grows the receiver shapes; all 15 kernel parity suites pass against the rebuilt kernel (147 tests). - Full `npm test` on this branch: 189 files, 3187 passed, 9 skipped, 0 failed. Re-index after upgrading. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
294 lines
5.0 KiB
Rust
294 lines
5.0 KiB
Rust
//! Torture fixture for the rust kernel walker (R7b) — every quirk in
|
|
//! docs/design/rust-lang-kernel-port-checklist.md, parse-clean.
|
|
|
|
use std::fmt;
|
|
use crate::mod_a::Item;
|
|
use crate::mod_b::{A, B as C, sub::D};
|
|
use a::{b::{c, d}};
|
|
use foo_single;
|
|
use std::collections::*;
|
|
|
|
/// Widget docs.
|
|
pub struct Widget {
|
|
pub n: u32,
|
|
name: String,
|
|
field: Deep,
|
|
}
|
|
|
|
pub struct Unit;
|
|
|
|
pub struct Pair(u32, u32);
|
|
|
|
/// Doc broken by the attribute below — must yield NO docstring.
|
|
#[derive(Debug)]
|
|
pub struct Doc {
|
|
x: u32,
|
|
}
|
|
|
|
pub struct Deep {
|
|
z: u32,
|
|
}
|
|
|
|
pub enum Shape {
|
|
Circle(f32),
|
|
Rect { w: f32, h: f32 },
|
|
Empty,
|
|
}
|
|
|
|
pub type Alias = Vec<Widget>;
|
|
|
|
/// Render trait docs.
|
|
pub trait Render: Base + fmt::Debug {
|
|
fn render(&self);
|
|
fn hint(&self) -> Size {
|
|
Size::default()
|
|
}
|
|
const CAP: usize = init_cap();
|
|
type Output;
|
|
}
|
|
|
|
pub trait Base {}
|
|
|
|
pub trait Super2: Producer<u32> {}
|
|
|
|
pub trait Owned: for<'de> Deserialize<'de> {}
|
|
|
|
impl Render for Widget {
|
|
fn render(&self) {
|
|
draw(self);
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Widget {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "w{}", self.n)
|
|
}
|
|
}
|
|
|
|
impl Widget {
|
|
const SCALE: usize = 3;
|
|
|
|
fn area(&self) -> u32 {
|
|
self.n * mul()
|
|
}
|
|
|
|
/// Receiver shapes (#1585): only `self.<field>.<method>()` keeps the
|
|
/// owner-field prefix; deeper / parenthesized / call / bare-self collapse.
|
|
fn via_field(&self) -> u32 {
|
|
self.field.deep_call();
|
|
self.field.z.clone();
|
|
self.method_a().chain_b();
|
|
(self.field).deep_call();
|
|
self.area()
|
|
}
|
|
|
|
fn clone_self(&self) -> Self {
|
|
Self::assoc();
|
|
Widget {
|
|
n: self.n,
|
|
name: String::new(),
|
|
field: Deep { z: 0 },
|
|
}
|
|
}
|
|
|
|
fn borrow_widget(&self) -> &Widget {
|
|
self
|
|
}
|
|
|
|
fn outer(&self) -> u32 {
|
|
fn inner_helper(v: u32) -> u32 {
|
|
v
|
|
}
|
|
inner_helper(self.n)
|
|
}
|
|
}
|
|
|
|
pub struct Container<T> {
|
|
item: T,
|
|
}
|
|
|
|
impl<T> Container<T> {
|
|
fn unwrap(self) -> T {
|
|
self.item
|
|
}
|
|
}
|
|
|
|
impl Render for Container<u32> {
|
|
fn render(&self) {}
|
|
}
|
|
|
|
/// Receiver = the impl_item's `type` field (#1588): generic, lifetime,
|
|
/// reference, scoped, and generic-trait impls all qualify by the TYPE.
|
|
pub trait Source {
|
|
fn read(&mut self) -> usize;
|
|
}
|
|
|
|
pub struct FileSource {
|
|
pub n: usize,
|
|
}
|
|
|
|
impl Source for FileSource {
|
|
fn read(&mut self) -> usize {
|
|
self.n
|
|
}
|
|
}
|
|
|
|
pub struct BufSource<T> {
|
|
pub inner: T,
|
|
}
|
|
|
|
impl<T> Source for BufSource<T> {
|
|
fn read(&mut self) -> usize {
|
|
0
|
|
}
|
|
}
|
|
|
|
pub struct Parents<'a> {
|
|
cur: &'a u32,
|
|
}
|
|
|
|
impl<'a> Iterator for Parents<'a> {
|
|
type Item = u32;
|
|
fn next(&mut self) -> Option<u32> {
|
|
None
|
|
}
|
|
}
|
|
|
|
impl<T: Clone> Container<T> {
|
|
fn dup(&self) -> T {
|
|
self.item.clone()
|
|
}
|
|
}
|
|
|
|
impl Base for &Widget {}
|
|
|
|
impl<T> Render for &mut BufSource<T> {
|
|
fn render(&self) {}
|
|
}
|
|
|
|
impl Base for self::Deep {}
|
|
|
|
impl From<u32> for FileSource {
|
|
fn from(n: u32) -> Self {
|
|
FileSource { n: n as usize }
|
|
}
|
|
}
|
|
|
|
impl Base for (u32, u32) {}
|
|
|
|
impl Render for dyn Base {
|
|
fn render(&self) {}
|
|
}
|
|
|
|
impl Base for u32 {}
|
|
|
|
impl Later {
|
|
fn touch(&self) {}
|
|
}
|
|
|
|
pub struct Later {
|
|
z: u32,
|
|
}
|
|
|
|
/* Block-doc for an async fn — isAsync must stay FALSE (dead-code hook). */
|
|
pub async fn fetch_data(url: &str) -> Result<Response, Error> {
|
|
let body = get(url).await;
|
|
client.request().await.send();
|
|
body
|
|
}
|
|
|
|
pub fn nested_ret() -> Result<Vec<Widget>, Error> {
|
|
make_result()
|
|
}
|
|
|
|
pub fn vec_ret(w: &Widget) -> Vec<Widget> {
|
|
build_list(w)
|
|
}
|
|
|
|
pub(crate) fn crate_fn() {}
|
|
|
|
fn caller() {
|
|
let w = Widget {
|
|
n: 1,
|
|
name: make_name(),
|
|
field: Deep { z: 1 },
|
|
};
|
|
let v = m::Widget { n: 2 };
|
|
let r = Foo::new().bar();
|
|
let x = w.method_a().chain_b();
|
|
let y = w.field.deep_call();
|
|
let s = "lit".len();
|
|
let f = 5.0_f64.floor();
|
|
helper();
|
|
m::helper2();
|
|
let t = helper::<u32>(3);
|
|
(helper)(1);
|
|
takes(Widget {
|
|
n: 3,
|
|
name: n2(),
|
|
field: Deep { z: 2 },
|
|
});
|
|
}
|
|
|
|
fn handler() {}
|
|
fn handler2() {}
|
|
fn cb_a() {}
|
|
fn cb_b() {}
|
|
fn invoke_all(fns: [fn(); 2]) {}
|
|
|
|
fn register(f: fn()) {
|
|
f();
|
|
}
|
|
|
|
pub struct Holder {
|
|
cb: fn(),
|
|
}
|
|
|
|
fn wiring(mut o: Holder) {
|
|
register(handler);
|
|
o.cb = handler2;
|
|
let h = Holder { cb: cb_a };
|
|
let arr = [cb_a, cb_b];
|
|
let local = handler;
|
|
let (t1, t2) = (cb_a, cb_b);
|
|
invoke_all(arr);
|
|
invoke(foo_single);
|
|
}
|
|
|
|
static CB: fn() = handler;
|
|
|
|
const MAX_LIMIT: u32 = OTHER_LIMIT;
|
|
const OTHER_LIMIT: u32 = 99;
|
|
|
|
fn reads_limits() -> u32 {
|
|
MAX_LIMIT + OTHER_LIMIT
|
|
}
|
|
|
|
fn shadowed_read() {
|
|
let MAX_LIMIT = 5;
|
|
let _ = MAX_LIMIT;
|
|
}
|
|
|
|
mod inner {
|
|
pub fn helper_pub() {}
|
|
fn hidden() {}
|
|
pub struct Item2 {
|
|
pub v: u32,
|
|
}
|
|
}
|
|
|
|
fn mount() {
|
|
let r = routes![a::b::index_h, health_h];
|
|
let c = catchers![not_found_h];
|
|
let skipped = rocket::routes![a::x];
|
|
}
|
|
|
|
routes![top_level_h];
|
|
|
|
pub union Reg {
|
|
pub raw: u32,
|
|
pub halves: [u16; 2],
|
|
}
|
|
|
|
impl Base for Reg {}
|