mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 19:30:10 +08:00
fix(rust): a call on what as_mut() hands back reaches the receiver's declared type (#2710)
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<Box<Sleep>> 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<P> 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a31bf2f476
commit
5a7a4347a6
@@ -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<YourType> 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>`, `self: Rc<Self>` or `self: Arc<Self>`, 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<P>`, 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<Box<Sleep>>` 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.
|
||||
|
||||
@@ -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<P>` re-pins what it points at, a
|
||||
* `Box<T>` lends its `T`, an `Option<T>` stays an `Option`, whose methods are
|
||||
* std's. By the name alone, tokio's `self.delay.as_mut().reset(now)` on a
|
||||
* `Pin<Box<Sleep>>` 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<string, string> = {
|
||||
'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<Box<T>>` and `Option<T>`.
|
||||
'src/interval.rs': [
|
||||
'use std::pin::Pin;',
|
||||
'',
|
||||
'use crate::sleep::Sleep;',
|
||||
'',
|
||||
'pub struct Interval {',
|
||||
' delay: Pin<Box<Sleep>>,',
|
||||
' timer: Option<Sleep>,',
|
||||
'}',
|
||||
'',
|
||||
'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<T>` and `NonNull<T>` 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<Sleep>,',
|
||||
'}',
|
||||
'',
|
||||
'impl Holder {',
|
||||
' pub fn check(&mut self) -> u64 {',
|
||||
' self.inner.as_mut().deadline()',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'pub struct Shared {',
|
||||
' entry: NonNull<Sleep>,',
|
||||
' next: Option<Sleep>,',
|
||||
'}',
|
||||
'',
|
||||
'impl Shared {',
|
||||
' pub unsafe fn when(&self) -> u64 {',
|
||||
' unsafe { self.entry.as_ref().deadline() }',
|
||||
' }',
|
||||
'',
|
||||
' pub fn upcoming(&mut self) -> Option<u64> {',
|
||||
' 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<W> {',
|
||||
' #[pin]',
|
||||
' inner: W,',
|
||||
' #[pin]',
|
||||
' delay: Sleep,',
|
||||
' timer: Option<Sleep>,',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'impl<W> Timeout<W> {',
|
||||
' 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<R: Unpin>(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<Box<T>>` 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<T>` is the `Option`\'s own, no project method', () => {
|
||||
expect(callsAt('src/interval.rs', 20)).toEqual([]);
|
||||
});
|
||||
|
||||
it('declared `Box<T>` or `NonNull<T>` 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<T>`, 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([]);
|
||||
});
|
||||
});
|
||||
+222
-17
@@ -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<Box<dyn Future>>` → `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<Timer> = …` — 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<Box<dyn
|
||||
* Future>>` → `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<P>`.
|
||||
* (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<P>`.
|
||||
*/
|
||||
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<Box<Sleep>>`
|
||||
* 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<T>`, `NonNull<T>` → `&mut T`. */
|
||||
const RUST_LENDING_POINTERS: ReadonlySet<string> = new Set(['Box', 'NonNull', 'Rc', 'Arc', 'Cow']);
|
||||
|
||||
/** A Rust type as written, by its last name — `&'a mut std::pin::Pin<Box<T>>` → `Pin` with `Box<T>` — 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<P>` points
|
||||
* at (rustPinnedCallOn), lends a `Box<T>`'s or a `NonNull<T>`'s `T`, and
|
||||
* turns an `Option<T>` 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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user