From b902f8f921f48a1d9341e0a6360ab182532d0980 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 20 Oct 2026 11:59:01 -0500 Subject: [PATCH] fix(rust): a call on what as_mut() hands back reaches the receiver's declared type A Rust call written x.as_mut().m(..) or x.as_ref().m(..) reaches the resolver by its bare name, so it was matched by name alone. tokio's self.delay.as_mut().reset(now) on a Pin> field linked to the calling Interval::reset, its this.timer.as_mut().as_pin_mut() on an Option to a stream's as_pin_mut, reader.as_mut().poll_read(..) on a generic R to BufReader::poll_read, and dioxus's self.child.as_mut().map(..) and self.server.as_mut().filter(..) on Options to its own GenerationalRefMut::map and Store::filter. Such a call is now read from the call's own chain and resolved on the receiver's declared type as as_mut passes it on: a Pin

re-pins what it points at (Rust's lookup through the Pin from the previous change), a Box, NonNull, Rc or Arc lends what it holds, an Option stays an Option, whose methods are std's, unless ? takes the value back out (self.client.as_mut()?.next()), and a type parameter or another std or dependency type gives no project method. A field is read from its struct's declaration, also through pin-project's let me = self.project() (a #[pin] field projects to Pin<&mut T>), even when a pin_project! macro wraps the struct; a local or parameter from its own declaration, which now also reads let x: T = .. annotations. A receiver whose type isn't written where it can be read, a pin! local or a call's result, is left to the name strategies as before. A/B on tokio, axum, clap, serde, ripgrep, gleam and dioxus: 33 wrong links removed, 7 moved to the right method, 13 right links added, none lost, and 28 same-target links now resolved from the declared type instead of by name. axum, clap, serde and the controls express, gin, flask and leveldb are byte-identical. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/rust-as-mut-chains.test.ts | 248 +++++++++++++++++++++++++++ src/resolution/name-matcher.ts | 239 ++++++++++++++++++++++++-- 3 files changed, 471 insertions(+), 17 deletions(-) create mode 100644 __tests__/rust-as-mut-chains.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b2659bd..cc8cbf817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -227,6 +227,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Rust, a call written `Self::new(…)`, `Self::from_std(…)` or `Self::Text(…)` now links to that function or variant of the type whose `impl` it is written in, even when other types have one of the same name. Before, such a call was linked only when nothing else in the project shared the name, so most `Self::new(…)` constructors and many helper calls were missing from callers and impact. Re-index Rust projects after upgrading. - In Rust, a call like `String::from("…")`, `Vec::from([…])` or `NonNull::from(x)` no longer links to your project's own `impl From for String` (or for `Vec`, `NonNull`, …) unless what it passes is a `YourType`. A method call on a standard-library value, like `files.iter()` or `buf.extend(…)`, links to your trait's implementation for that type only where the trait is imported. Before, every `String::from("…")` call in dioxus's CLI linked to one such conversion, and tokio's `vec.iter()` calls to an internal trait's `Vec` impl. Re-index Rust projects after upgrading. - In Rust, a method called through `self: Pin<&mut Self>`, `self: Box`, `self: Rc` or `self: Arc`, or on a value declared `Pin<&mut …>`, now links the way Rust looks it up: to the type's own method, like `self.poll_flush(cx)` inside tokio's `impl AsyncWrite for File`, rather than to a catch-all `impl AsyncWrite for Pin

`, while `Pin`'s own methods, like `self.get_mut()`, stay unlinked. A type's own method now also comes before a trait's of the same name, so tokio's `Arc::from_raw(…)` calls are the standard library's rather than its internal `Link` trait's, and a method called on a tuple, like serde's `(self.ip(), self.port()).serialize(…)`, no longer links to an unrelated type's. Before, most calls made through such a `self` linked to nothing. Re-index Rust projects after upgrading. +- In Rust, a method called on what `.as_mut()` or `.as_ref()` hands back now links by the type its receiver is declared with: `self.delay.as_mut().reset(…)` on a `Pin>` field reaches `Sleep::reset`, a field pin-project hands out reaches its own type's method, and a `Box` or `NonNull` lends what it holds. A method on an `Option` reached this way, like `self.child.as_mut().map(…)`, or on a generic type no longer links to an unrelated project method that shares its name. Before, these calls were matched by name alone, so tokio's `self.delay.as_mut().reset(…)` linked to the calling `Interval::reset`, and dioxus's `Option` calls to its own `map` and `filter` helpers. Re-index Rust projects after upgrading. - In Rust, a type alias, struct, enum or function declared without `pub` now links only from its own module and the modules inside it, and a name a `use` brings in from one of your workspace's crates, like `use tokio_test::task::spawn` or `use crate::error::Error`, links only to an item of that crate or of a crate it depends on. Before, a file's `use crate::Result` could link its `Result<…>` types to another module's private `type Result`, as in dioxus's CLI, and a call to an imported function could link to another crate's function of the same name, such as tokio's test `spawn(…)` calls to one in its examples, or to nothing because a private function of that name was in the way. Re-index Rust projects after upgrading. - In Rust, a name written on its own, like an `Err(…)` call or a `Response` return type, or through a module, like `io::Error` or `use crate::de::Error`, no longer links to a `type` or `const` declared inside an `impl`, such as a `FromStr` impl's `type Err` or a `Service` impl's `type Response`: only a path like `Self::Err` names those, and they are now listed under their type, as `ValueHint::Err`. Before, serde's and ripgrep's `Ok(…)` and `Err(…)` calls linked to a serializer's or parser's `type Ok` or `type Err`, axum's handlers' `Response` to a `Service` impl's, and ripgrep's `impl Captures for RegexCaptures` to another impl's `type Captures` instead of the `Captures` trait. Re-index Rust projects after upgrading. - In C++, every name a declaration lists now has the declaration's type, like `strings_found` in google/breakpad's `ByteBuffer symbols_found, strings_found;` or `data_` in a class's `Table *index_, *data_;`, so calls on it link to that class's methods. Before, only the first name had its type read, and a call on another name linked to nothing or to the class of an earlier variable with the same name. diff --git a/__tests__/rust-as-mut-chains.test.ts b/__tests__/rust-as-mut-chains.test.ts new file mode 100644 index 000000000..2841f2a47 --- /dev/null +++ b/__tests__/rust-as-mut-chains.test.ts @@ -0,0 +1,248 @@ +/** + * A Rust call on what `.as_mut()` or `.as_ref()` hands back, which the + * extractor records by its bare name, is a call on the receiver's declared + * type as `as_mut` passes it on: a `Pin

` re-pins what it points at, a + * `Box` lends its `T`, an `Option` stays an `Option`, whose methods are + * std's. By the name alone, tokio's `self.delay.as_mut().reset(now)` on a + * `Pin>` field went to the calling `Interval::reset`, its + * `this.timer.as_mut().as_pin_mut()` on an `Option` to a stream's + * `as_pin_mut`, `reader.as_mut().poll_read(…)` on a generic `R` to + * `BufReader::poll_read`, and dioxus's `self.child.as_mut().map(…)` to a + * `GenerationalRefMut::map` that shared the word `mut`. + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-as-mut-')); + const files: Record = { + 'Cargo.toml': '[package]\nname = "rt"\nversion = "0.1.0"\n', + 'src/main.rs': 'mod copy;\nmod decoys;\nmod holder;\nmod interval;\nmod projected;\nmod pump;\nmod sleep;\n\nfn main() {}\n', + 'src/sleep.rs': [ + 'use std::pin::Pin;', + '', + 'pub struct Sleep {', + ' deadline: u64,', + '}', + '', + 'impl Sleep {', + ' pub fn reset(self: Pin<&mut Self>, deadline: u64) {}', + '', + ' pub fn deadline(&self) -> u64 {', + ' self.deadline', + ' }', + '}', + '', + ].join('\n'), + // Methods by the names the calls below use, on types the calls never reach. + 'src/decoys.rs': [ + 'use std::pin::Pin;', + '', + 'pub struct Writer;', + '', + 'impl Writer {', + ' pub fn start_seek(self: Pin<&mut Self>, pos: u64) {}', + '}', + '', + 'pub struct GuardMut;', + '', + 'impl GuardMut {', + ' pub fn map(self, f: u8) -> u8 {', + ' f', + ' }', + '}', + '', + 'pub struct Lines;', + '', + 'impl Lines {', + ' pub fn as_pin_mut(self: Pin<&mut Self>) {}', + '}', + '', + ].join('\n'), + // Fields declared `Pin>` and `Option`. + 'src/interval.rs': [ + 'use std::pin::Pin;', + '', + 'use crate::sleep::Sleep;', + '', + 'pub struct Interval {', + ' delay: Pin>,', + ' timer: Option,', + '}', + '', + 'impl Interval {', + ' pub fn reset(&mut self, at: u64) {', + ' self.delay.as_mut().reset(at);', + ' }', + '', + ' pub fn peek(&mut self) -> u64 {', + ' self.delay.as_ref().deadline()', + ' }', + '', + ' pub fn stop(&mut self) {', + ' self.timer.as_mut().map(|_| ());', + ' }', + '}', + '', + ].join('\n'), + // Fields declared `Box` and `NonNull` lend the `T`; `?` takes an `Option`'s back out. + 'src/holder.rs': [ + 'use std::ptr::NonNull;', + '', + 'use crate::sleep::Sleep;', + '', + 'pub struct Holder {', + ' inner: Box,', + '}', + '', + 'impl Holder {', + ' pub fn check(&mut self) -> u64 {', + ' self.inner.as_mut().deadline()', + ' }', + '}', + '', + 'pub struct Shared {', + ' entry: NonNull,', + ' next: Option,', + '}', + '', + 'impl Shared {', + ' pub unsafe fn when(&self) -> u64 {', + ' unsafe { self.entry.as_ref().deadline() }', + ' }', + '', + ' pub fn upcoming(&mut self) -> Option {', + ' Some(self.next.as_mut()?.deadline())', + ' }', + '}', + '', + ].join('\n'), + // pin-project: a `#[pin]` field projects to `Pin<&mut T>`, another to `&mut T`. + 'src/projected.rs': [ + 'use std::pin::Pin;', + '', + 'use crate::sleep::Sleep;', + '', + 'pin_project_lite::pin_project! {', + ' pub struct Timeout {', + ' #[pin]', + ' inner: W,', + ' #[pin]', + ' delay: Sleep,', + ' timer: Option,', + ' }', + '}', + '', + 'impl Timeout {', + ' pub fn poll_seek(self: Pin<&mut Self>, pos: u64) {', + ' let mut me = self.project();', + ' me.inner.as_mut().start_seek(pos);', + ' me.delay.as_mut().reset(pos);', + ' me.timer.as_mut().as_pin_mut();', + ' }', + '}', + '', + ].join('\n'), + // Values declared `Pin<&mut …>`. + 'src/copy.rs': [ + 'use std::pin::Pin;', + '', + 'use crate::sleep::Sleep;', + '', + 'pub fn read_some(mut reader: Pin<&mut R>, deadline: Pin<&mut Sleep>) -> u64 {', + ' reader.as_mut().start_seek(0);', + ' deadline.as_ref().deadline()', + '}', + '', + ].join('\n'), + // `self: Pin<&mut Self>`: `self.as_mut()` is that same `Pin`. + 'src/pump.rs': [ + 'use std::pin::Pin;', + '', + 'pub struct Pump;', + '', + 'impl Pump {', + ' fn step(self: Pin<&mut Self>) {}', + '', + ' fn get_mut(&mut self) -> u8 {', + ' 0', + ' }', + '', + ' pub fn run(mut self: Pin<&mut Self>) {', + ' self.as_mut().step();', + ' self.as_mut().get_mut();', + ' }', + '}', + '', + ].join('\n'), + }; + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + cg = await CodeGraph.init(root, { index: true }); +}, 60_000); + +afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +/** What the calls on `line` of `file` link to, as `qualifiedName@file:line`, sorted. */ +const callsAt = (file: string, line: number): string[] => { + const out: string[] = []; + for (const node of cg.getNodesInFile(file)) { + for (const edge of cg.getOutgoingEdges(node.id)) { + if (edge.kind !== 'calls' || edge.line !== line) continue; + const target = cg.getNode(edge.target)!; + out.push(`${target.qualifiedName}@${target.filePath}:${target.startLine}`); + } + } + return [...new Set(out)].sort(); +}; + +describe('a Rust call on `.as_mut()` / `.as_ref()` of a field', () => { + it('declared `Pin>` is `T`\'s method', () => { + expect(callsAt('src/interval.rs', 12)).toEqual(['Sleep::reset@src/sleep.rs:8']); + expect(callsAt('src/interval.rs', 16)).toEqual(['Sleep::deadline@src/sleep.rs:10']); + }); + + it('declared `Option` is the `Option`\'s own, no project method', () => { + expect(callsAt('src/interval.rs', 20)).toEqual([]); + }); + + it('declared `Box` or `NonNull` is `T`\'s method', () => { + expect(callsAt('src/holder.rs', 11)).toEqual(['Sleep::deadline@src/sleep.rs:10']); + expect(callsAt('src/holder.rs', 22)).toEqual(['Sleep::deadline@src/sleep.rs:10']); + }); + + it('declared `Option`, through `?`, is `T`\'s method', () => { + expect(callsAt('src/holder.rs', 26)).toEqual(['Sleep::deadline@src/sleep.rs:10']); + }); + + it('projected by pin-project is the field\'s type, pinned or not', () => { + expect(callsAt('src/projected.rs', 18)).toEqual([]); + expect(callsAt('src/projected.rs', 19)).toEqual(['Sleep::reset@src/sleep.rs:8']); + expect(callsAt('src/projected.rs', 20)).toEqual([]); + }); +}); + +describe('a Rust call on `.as_mut()` / `.as_ref()` of a value declared `Pin<&mut T>`', () => { + it('is `T`\'s method, and nothing for a type parameter', () => { + expect(callsAt('src/copy.rs', 6)).toEqual([]); + expect(callsAt('src/copy.rs', 7)).toEqual(['Sleep::deadline@src/sleep.rs:10']); + }); +}); + +describe('a Rust call on `self.as_mut()` through `self: Pin<&mut Self>`', () => { + it('is the type\'s own method, or `Pin`\'s', () => { + expect(callsAt('src/pump.rs', 13)).toEqual(['Pump::step@src/pump.rs:6']); + expect(callsAt('src/pump.rs', 14)).toEqual([]); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 3d43d4504..d0d789d46 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -17679,38 +17679,80 @@ function rustImplFor(method: Node, inner: string | null, context: ResolutionCont return impl !== null && impl.selfArgs.some((a) => impl.params.includes(a) || a === inner); } +/** The type written at the start of `text`, up to what ends it at its own depth: `Pin<&mut R>, cx: …` → `Pin<&mut R>`. */ +function rustTypeTextAt(text: string): string { + let depth = 0; + for (let i = 0; i < text.length; i++) { + const ch = text[i]!; + if (ch === '<' || ch === '(' || ch === '[') depth++; + else if (ch === '>' && text[i - 1] !== '-') depth = Math.max(0, depth - 1); + else if (ch === ')' || ch === ']') { + if (depth === 0) return text.slice(0, i).trim(); + depth--; + } else if (depth === 0 && (ch === ',' || ch === '=' || ch === ';' || ch === '{')) return text.slice(0, i).trim(); + } + return text.trim(); +} + /** - * The type a Rust value declared `Pin<…>` pins — `read: Pin<&mut R>` → `R`, - * `fut: Pin>` → `Future`, `this: Pin<&mut Self>` → the impl's - * type — read off its nearest declaration in the calling function; null when - * that names none (`let fut = Pin::new(&mut f)`). + * The type a Rust local or parameter is written with — `reader: Pin<&mut + * R>`, `let timer: Option = …` — read off its nearest declaration in + * the calling function; null when that declaration writes none (`let f = + * pin!(f)`). */ -function rustPinnedType(receiver: string, ref: UnresolvedRef, context: ResolutionContext): string | null { +function rustDeclaredTypeText(name: string, ref: UnresolvedRef, context: ResolutionContext): string | null { const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? []; const start = Math.max(0, enclosingScopeStartLine(ref, context) - 1); - const escaped = receiver.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const declared = new RegExp(`\\b${escaped}\\s*:(?!:)\\s*(.*)$|\\blet\\s+(?:mut\\s+)?${escaped}\\b`); + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const declared = new RegExp(`\\blet\\s+(?:mut\\s+)?${escaped}\\b(?:\\s*:(?!:)\\s*(.*))?|\\b${escaped}\\s*:(?!:)\\s*(.*)$`); for (let i = Math.min(lines.length, ref.line) - 1; i >= start; i--) { const m = declared.exec(lines[i] ?? ''); if (!m) continue; - const pinned = /^(?:&\s*(?:'\w+\s+)?(?:mut\s+)?)?(?:(?:std|core)\s*::\s*pin\s*::\s*)?Pin\s*<\s*(?:&\s*(?:'\w+\s+)?(?:mut\s+)?|(?:(?:std|alloc)\s*::\s*boxed\s*::\s*)?Box\s*<\s*)?(?:dyn\s+|impl\s+)?((?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*)/ - .exec(m[1] ?? ''); - const type = pinned?.[1]!.split('::').pop()!.trim() ?? null; - if (type !== 'Self') return type; - return rustSelfType(ref, context)?.type ?? null; + const written = m[1] ?? m[2]; + return written === undefined ? null : rustTypeTextAt(written); } return null; } +/** + * What a Rust `Pin<…>` type pins — `Pin<&mut R>` → `R`, `Pin>` → `Future`, `Pin<&mut Self>` → the impl's type; null for a type + * that is no `Pin`. + */ +function rustPinnedPointee(type: string, ref: UnresolvedRef, context: ResolutionContext): string | null { + const pinned = /^(?:&\s*(?:'\w+\s+)?(?:mut\s+)?)?(?:(?:std|core)\s*::\s*pin\s*::\s*)?Pin\s*<\s*(?:&\s*(?:'\w+\s+)?(?:mut\s+)?|(?:(?:std|alloc)\s*::\s*boxed\s*::\s*)?Box\s*<\s*)?(?:dyn\s+|impl\s+)?((?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*)/ + .exec(type); + const name = pinned?.[1]!.split('::').pop()!.trim() ?? null; + return name === 'Self' ? rustSelfType(ref, context)?.type ?? null : name; +} + +/** + * The type a Rust value declared `Pin<…>` pins — `read: Pin<&mut R>` → `R`, + * `this: Pin<&mut Self>` → the impl's type — read off its nearest + * declaration in the calling function; null when that names none (`let fut + * = Pin::new(&mut f)`). + */ +function rustPinnedType(receiver: string, ref: UnresolvedRef, context: ResolutionContext): string | null { + const type = rustDeclaredTypeText(receiver, ref, context); + return type === null ? null : rustPinnedPointee(type, ref, context); +} + /** * A Rust call on a value declared `Pin<…>`: Rust's lookup through the `Pin` - * (rustThroughWrapper) on the type it pins. A type parameter's own methods - * are its bound's, which the call doesn't name: tokio's `read.poll_read(…)` - * on `read: Pin<&mut R>` went to its `impl AsyncRead for Pin

`. + * (rustThroughWrapper) on the type it pins. */ function matchRustPinnedCall(receiver: string, name: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { - const pinned = rustPinnedType(receiver, ref, context); - const inner = pinned !== null && !isRustTypeParam(pinned, ref, context) ? pinned : null; + return rustPinnedCallOn(rustPinnedType(receiver, ref, context), name, ref, context); +} + +/** + * Rust's lookup through a `Pin` (rustThroughWrapper) for `name`, on the type + * it pins. A type parameter's own methods are its bound's, which the call + * doesn't name: tokio's `read.poll_read(…)` on `read: Pin<&mut R>` went to + * its `impl AsyncRead for Pin

`. + */ +function rustPinnedCallOn(pointee: string | null, name: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { + const inner = pointee !== null && !isRustTypeParam(pointee, ref, context) ? pointee : null; const own = inner === null ? [] : context.getNodesByName(name).filter((n) => n.language === 'rust' && n.kind === 'method' && (n.qualifiedName === `${inner}::${name}` || n.qualifiedName.endsWith(`::${inner}::${name}`))); const through = rustThroughWrapper('Pin', name, own, inner, ref, context); @@ -17722,6 +17764,160 @@ function matchRustPinnedCall(receiver: string, name: string, ref: UnresolvedRef, return target ? { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'instance-method' } : null; } +/** + * A Rust call on what `.as_mut()` or `.as_ref()` hands back — `self.delay + * .as_mut().reset(…)`, `this.timer.as_mut().as_pin_mut()`, `reader.as_mut() + * .poll_read(…)` — which the extractor records by its bare name: a call on + * the receiver's declared type as `as_mut` passes it on (rustLentCall). A + * field is read from its struct's declaration, through pin-project's `let + * me = self.project()` too, a local or a parameter from its own. By the name + * alone, tokio's `self.delay.as_mut().reset(now)` on a `Pin>` + * field went to the calling `Interval::reset`, and dioxus's + * `self.child.as_mut().map(…)` on an `Option` to a `GenerationalRefMut::map`. + * Undefined, for the name strategies to decide as before, when the + * receiver's type isn't written where it can be read: a `pin!` local, a + * call's result. + */ +function matchRustAsMutChainCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { + const line = (context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/))?.[ref.line - 1]; + if (!line || !/\.\s*as_(?:mut|ref)\s*\(\s*\)/.test(line)) return undefined; + const name = ref.referenceName; + // The call's expression starts at its column with the whole chain: + // `self.delay.as_mut().reset(` — `?` after `as_mut()` taking what an + // `Option` / `Result` holds back out (`self.client.as_mut()?.next()`). + const lent = new RegExp(`^(self|[a-z_]\\w*)(?:\\s*\\.\\s*([a-z_]\\w*))?\\s*\\.\\s*as_(?:mut|ref)\\s*\\(\\s*\\)\\s*(\\?)?\\s*\\.\\s*${name}\\s*(?:\\(|::<)`) + .exec(line.slice(Math.max(0, ref.column))); + if (!lent) return undefined; + const [, base, field, question] = lent; + const tried = question !== undefined; + const caller = context.getNodeById?.(ref.fromNodeId); + if (!caller) return undefined; + if (field === undefined) { + // `self.as_mut()` through `self: Pin<&mut Self>` is that same `Pin`. + if (base === 'self') return rustSelfWrapper(caller) === 'Pin' && !tried ? matchRustSelfCall(name, ref, context) : undefined; + const declared = rustDeclaredTypeText(base!, ref, context); + return declared === null ? undefined : rustLentCall(declared, name, ref, context, tried); + } + // A field of the type `self` is, or one pin-project hands out: + // `Pin<&mut T>` for a `#[pin]` field, `&mut T` for another. + const projected = base !== 'self'; + if (projected && !rustProjectsSelf(base!, ref, context)) return undefined; + const sep = caller.qualifiedName.lastIndexOf('::'); + const owner = sep > 0 ? caller.qualifiedName.slice(0, sep).split('::').pop() : undefined; + const decl = owner ? rustFieldDeclaration(owner, field, ref, context) : null; + if (!decl) return undefined; + return rustLentCall(projected && decl.pinned ? `Pin<&mut ${decl.type}>` : decl.type, name, ref, context, tried); +} + +/** Std pointers whose `as_ref` / `as_mut` lend what they hold: `Box`, `NonNull` → `&mut T`. */ +const RUST_LENDING_POINTERS: ReadonlySet = new Set(['Box', 'NonNull', 'Rc', 'Arc', 'Cow']); + +/** A Rust type as written, by its last name — `&'a mut std::pin::Pin>` → `Pin` with `Box` — after `&`, `dyn` and `impl`. */ +function rustTypeHead(type: string): { outer: string; args: string; text: string } | null { + const text = type.replace(/^(?:&\s*(?:'\w+\s+)?(?:mut\s+)?)+/, '').replace(/^(?:dyn|impl)\s+/, '').trim(); + const m = /^(?:[A-Za-z_]\w*\s*::\s*)*([A-Za-z_]\w*)\s*(?:<([\s\S]*)>)?/.exec(text); + return m ? { outer: m[1]!, args: m[2] ?? '', text } : null; +} + +/** + * What `name` called on `x.as_mut()` / `x.as_ref()` reaches for an `x` of + * the Rust type `type` as written: `as_mut` re-pins what a `Pin

` points + * at (rustPinnedCallOn), lends a `Box`'s or a `NonNull`'s `T`, and + * turns an `Option` into an `Option` again, whose methods are std's — + * unless `?` takes the `T` back out (`tried`: `self.client.as_mut()?.next()`). + * A type parameter's lends what its bound says, another std type's a std + * type: nothing of the project's either. Undefined for a project type's own + * `as_mut`, whose return type isn't read here. + */ +function rustLentCall(type: string, name: string, ref: UnresolvedRef, context: ResolutionContext, tried: boolean): ResolvedRef | null | undefined { + const head = rustTypeHead(type); + if (!head) return undefined; + if (head.outer === 'Pin' && !isRustProjectNamed('Pin', RUST_DECLARED_TYPE_KINDS, context)) { + return tried ? undefined : rustPinnedCallOn(rustPinnedPointee(head.text, ref, context), name, ref, context); + } + if (isRustProjectNamed(head.outer, RUST_DECLARED_TYPE_KINDS, context)) return undefined; + if (RUST_LENDING_POINTERS.has(head.outer)) return tried ? undefined : rustCallOnType(rustTypeTextAt(head.args), name, ref, context); + if (head.outer === 'Option' || head.outer === 'Result') return tried ? rustCallOnType(rustTypeTextAt(head.args), name, ref, context) : null; + if (RUST_STD_TYPES.has(head.outer) || isRustTypeParam(head.outer, ref, context)) return null; + return undefined; +} + +/** + * What `name` called on a value of the Rust type `type` as written reaches: + * through a `Pin` (rustPinnedCallOn), through a `Box` or another std pointer + * to what it holds unless the pointer has the method itself, a project + * type's (or trait's) own method. A type parameter, std's or a dependency's + * type: nothing of the project's. Undefined for a type no declaration names. + */ +function rustCallOnType(type: string, name: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { + const head = rustTypeHead(type); + if (!head) return undefined; + if (head.outer === 'Pin' && !isRustProjectNamed('Pin', RUST_DECLARED_TYPE_KINDS, context)) { + return rustPinnedCallOn(rustPinnedPointee(head.text, ref, context), name, ref, context); + } + const outer = head.outer === 'Self' ? rustSelfType(ref, context)?.type : head.outer; + if (!outer || isRustTypeParam(outer, ref, context)) return null; + if (RUST_LENDING_POINTERS.has(outer) && !isRustProjectNamed(outer, RUST_DECLARED_TYPE_KINDS, context)) { + return RUST_WRAPPER_OWN_METHODS.get(outer)?.has(name) ? null : rustCallOnType(rustTypeTextAt(head.args), name, ref, context); + } + if (isRustOutsideSelf({ type: outer, root: null, trait: null, isTrait: false }, ref, context)) return null; + if (!isRustProjectNamed(outer, RUST_DECLARED_TYPE_KINDS, context)) return undefined; + return resolveMethodOnType(outer, name, ref, context, 0.85, 'instance-method'); +} + +/** Whether a Rust local is bound by pin-project's `let me = self.project()` (or `self.as_mut().project()`) in the calling function. */ +function rustProjectsSelf(name: string, ref: UnresolvedRef, context: ResolutionContext): boolean { + const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? []; + const start = Math.max(0, enclosingScopeStartLine(ref, context) - 1); + const bound = new RegExp(`\\blet\\s+(?:mut\\s+)?${name}\\b\\s*=\\s*(.*)$`); + for (let i = Math.min(lines.length, ref.line) - 1; i >= start; i--) { + const m = bound.exec(lines[i] ?? ''); + if (m) return /^self\s*(?:\.\s*as_mut\s*\(\s*\)\s*)?\.\s*project\s*\(\s*\)/.test(m[1]!); + } + return false; +} + +/** + * How a field of the Rust type `owner` is declared: its type as written, and + * whether pin-project pins it (`#[pin]`). Read from the struct's own lines — + * the struct's node, or, for one a macro wraps (`pin_project! { struct + * Sleep { … } }`), which the index doesn't hold, its text in the calling + * file. Null when no declaration of the field is found. + */ +function rustFieldDeclaration(owner: string, field: string, ref: UnresolvedRef, context: ResolutionContext): { type: string; pinned: boolean } | null { + const fileLines = (file: string): string[] => context.getFileLines?.(file) ?? context.readFile(file)?.split(/\r?\n/) ?? []; + const spans = preferCallSiteFile(context.getNodesByName(owner).filter((n) => + n.language === 'rust' && (n.kind === 'struct' || n.kind === 'union')), ref.filePath) + .map((s) => fileLines(s.filePath).slice(s.startLine - 1, s.endLine)); + if (spans.length === 0) { + const lines = fileLines(ref.filePath); + const header = new RegExp(`\\bstruct\\s+${owner}\\b`); + const at = lines.findIndex((l) => header.test(l.replace(/\/\/.*$/, ''))); + if (at >= 0) { + let depth = 0; + let end = at; + for (let opened = false; end < lines.length; end++) { + for (const ch of (lines[end] ?? '').replace(/\/\/.*$/, '')) { + if (ch === '{') { depth++; opened = true; } else if (ch === '}') depth--; + } + if (opened && depth <= 0) break; + } + spans.push(lines.slice(at, end + 1)); + } + } + const declared = new RegExp(`(?:^|[\\s{(,])(?:pub(?:\\([^)]*\\))?\\s+)?${field}\\s*:(?!:)\\s*(.*)$`); + for (const span of spans) { + for (let i = 0; i < span.length; i++) { + const line = (span[i] ?? '').replace(/\/\/.*$/, ''); + const m = declared.exec(line); + if (!m) continue; + const pinned = /#\[pin\]/.test(line.slice(0, m.index + 1)) || /^\s*#\[pin\]\s*$/.test(span[i - 1] ?? ''); + return { type: rustTypeTextAt(m[1]!), pinned }; + } + } + return null; +} + /** * Resolve a Rust call through a field of the enclosing type — * `self.inner.run()`, emitted by the extractor as `self.inner.run` (#1585). @@ -19962,6 +20158,15 @@ function matchReferenceInner( if (chained !== undefined) return chained; } + // A Rust call on what `.as_mut()` / `.as_ref()` hands back, which the + // extractor records by its bare name — `self.delay.as_mut().reset(…)` — is + // a call on the type the receiver is declared with, or on nothing of the + // project's (matchRustAsMutChainCall). + if (ref.language === 'rust' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName)) { + const lent = nmTimedT('rustAsMut', ref, () => matchRustAsMutChainCall(ref, context)); + if (lent !== undefined) return lent; + } + // Try strategies in order of confidence let result: ResolvedRef | null;