fix(rust): a call through a Pin, Rc or Box self reaches the method Rust picks, inherent first (#2701)

A Rust method called through a self declared Pin<&mut Self>, Box<Self>,
Rc<Self> or Arc<Self> was typed by its wrapper. tokio's
self.poll_flush(cx) in impl AsyncWrite for File went to its blanket impl
AsyncWrite for Pin<P>, whose poll_flush would need a Pin<&mut Pin<...>>,
and nearly every other such call linked to nothing: tokio's
self.poll_write_priv(..) and self.get_pin_mut(), dioxus's
self.in_scope(..) through self: &Rc<Self>.

Such a call is now looked up as Rust does: the type's own method taking
self through that wrapper first, then the wrapper's own (Pin::get_mut,
Rc's clone: std's, which the index doesn't hold, or a project impl on the
wrapper taking &self), then the type's other methods by deref. A value
declared Pin<&mut T> is looked up the same way on T, so
read.poll_read(..) on a generic R no longer reaches the Pin<P> impl.

A type's inherent method comes before a trait's of the same name, as in
Rust: on a path (tokio's Arc::from_raw(ptr) is std's, not its impl Link
for Arc<ScheduledIo>'s), on a typed value, and through self, where each
of several trait impls for one type calls its own block's method. In a
trait impl, fn wake(self) { self.wake() } hands the call on to the
type's own method, never back to itself, and (self.f)(x) calls what a
field holds, no method. A method called on a tuple, serde's
(self.ip(), self.port()).serialize(s), is no named type's.

A/B on tokio, axum, clap, serde, ripgrep, gleam and dioxus: 30 wrong
links removed, 13 moved to the right target, 74 right links added, none
lost. express, gin, flask and leveldb are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-20 16:32:16 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 8beab37c46
commit f2f5dee044
3 changed files with 838 additions and 22 deletions
+514
View File
@@ -0,0 +1,514 @@
/**
* A Rust method call reaches what Rust's method lookup picks:
* - through a wrapper — `self: Pin<&mut Self>`, `self: Rc<Self>`, `read:
* Pin<&mut R>` — the wrapped type's own method taking `self` through it
* first (`fn poll_flush(self: Pin<&mut Self>, …)`), then the wrapper's own
* (`Pin::get_mut`, `Rc`'s `clone`, an impl on `Pin<P>` taking `&self`), then
* the wrapped type's other methods. tokio's `self.poll_flush(cx)` in `impl
* AsyncWrite for File` went to its blanket `impl AsyncWrite for Pin<P>`,
* whose `poll_flush` would need a `Pin<&mut Pin<…>>`;
* - a type's inherent method before a trait's of the same name: tokio's
* `Arc::from_raw(ptr)` is std's own, not its `impl Link for Arc<…>`'s, and
* a trait impl's `fn wake(self) { self.wake() }` hands the call on to the
* type's own `wake`, never back to itself;
* - `(self.into_route)(state)` calls what a field holds, no method;
* - a tuple's method is no named type's: serde's `(self.ip(),
* self.port()).serialize(s)` went to the `impl Serialize for Path` below it.
*/
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-wrapped-self-'));
const files: Record<string, string> = {
'Cargo.toml': '[package]\nname = "rt"\nversion = "0.1.0"\n',
'src/main.rs': 'mod copy;\nmod cursor;\nmod erased;\nmod file;\nmod grid;\nmod io;\nmod link;\nmod measure;\nmod net;\nmod runtime;\nmod ser;\nmod shape;\nmod sleep;\nmod tester;\nmod wake;\nmod waker;\n\nfn main() {}\n',
// A field holding a function, called through `self: Box<Self>`, beside a method of its name.
'src/erased.rs': [
'pub struct MakeErased {',
' into_route: fn(u8) -> u8,',
'}',
'',
'impl MakeErased {',
' fn into_route(self: Box<Self>, state: u8) -> u8 {',
' (self.into_route)(state)',
' }',
'',
' fn call(self: Box<Self>, state: u8) -> u8 {',
' self.into_route(state)',
' }',
'}',
'',
].join('\n'),
// A trait impl handing a call on to the type's own method of its name.
'src/waker.rs': [
'use std::task::Waker;',
'',
'trait WakerRef {',
' fn wake(self);',
'}',
'',
'impl WakerRef for Waker {',
' fn wake(self) {',
' self.wake();',
' }',
'}',
'',
'impl WakerRef for &Waker {',
' fn wake(self) {',
' self.wake_by_ref();',
' }',
'}',
'',
].join('\n'),
'src/measure.rs': [
'use crate::grid::Grid;',
'',
'pub trait Measure {',
' fn len(&self) -> usize;',
'}',
'',
'impl Measure for Grid {',
' fn len(&self) -> usize {',
' self.len()',
' }',
'}',
'',
].join('\n'),
'src/tester.rs': [
'use std::future::Future;',
'use std::pin::Pin;',
'use std::task::{Context, Poll};',
'',
'pub struct Tester {',
' done: bool,',
'}',
'',
'impl Future for Tester {',
' type Output = ();',
'',
' fn poll(mut self: Pin<&mut Self>, cx: &mut Context<\'_>) -> Poll<()> {',
' if self.done {',
' return Poll::Ready(());',
' }',
' self.done = true;',
' self.poll(cx)',
' }',
'}',
'',
].join('\n'),
'src/io.rs': [
'use std::pin::Pin;',
'use std::task::{Context, Poll};',
'',
'pub trait AsyncWrite {',
' fn poll_write(self: Pin<&mut Self>, cx: &mut Context<\'_>, buf: &[u8]) -> Poll<usize>;',
' fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<\'_>) -> Poll<()>;',
' fn poll_write_vectored(self: Pin<&mut Self>, cx: &mut Context<\'_>, buf: &[u8]) -> Poll<usize> {',
' self.poll_write(cx, buf)',
' }',
' fn is_write_vectored(&self) -> bool {',
' false',
' }',
'}',
'',
'pub trait AsyncRead {',
' fn poll_read(self: Pin<&mut Self>, cx: &mut Context<\'_>, buf: &mut [u8]) -> Poll<usize>;',
'}',
'',
'impl<P> AsyncWrite for Pin<P>',
'where',
' P: std::ops::DerefMut,',
' P::Target: AsyncWrite,',
'{',
' fn poll_write(self: Pin<&mut Self>, cx: &mut Context<\'_>, buf: &[u8]) -> Poll<usize> {',
' Poll::Ready(buf.len())',
' }',
'',
' fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<\'_>) -> Poll<()> {',
' Poll::Ready(())',
' }',
'',
' fn is_write_vectored(&self) -> bool {',
' true',
' }',
'}',
'',
'impl<P> AsyncRead for Pin<P>',
'where',
' P: std::ops::DerefMut,',
' P::Target: AsyncRead,',
'{',
' fn poll_read(self: Pin<&mut Self>, cx: &mut Context<\'_>, buf: &mut [u8]) -> Poll<usize> {',
' Poll::Ready(0)',
' }',
'}',
'',
].join('\n'),
'src/file.rs': [
'use std::pin::Pin;',
'use std::task::{Context, Poll};',
'',
'use crate::io::AsyncWrite;',
'',
'pub struct File {',
' pending: usize,',
'}',
'',
'impl File {',
' pub fn get_mut(&mut self) -> &mut usize {',
' &mut self.pending',
' }',
'',
' fn complete(&mut self) -> usize {',
' self.pending',
' }',
'}',
'',
'impl AsyncWrite for File {',
' fn poll_write(self: Pin<&mut Self>, cx: &mut Context<\'_>, buf: &[u8]) -> Poll<usize> {',
' Poll::Ready(buf.len())',
' }',
'',
' fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<\'_>) -> Poll<()> {',
' let _ = self.complete();',
' let me = self.get_mut();',
' Poll::Ready(())',
' }',
'',
' fn poll_write_vectored(self: Pin<&mut Self>, cx: &mut Context<\'_>, buf: &[u8]) -> Poll<usize> {',
' self.poll_flush(cx);',
' Poll::Ready(0)',
' }',
'}',
'',
].join('\n'),
// Two impls of one trait for one outside type: each `self.poll_flush` is its own block's.
'src/cursor.rs': [
'use std::io::Cursor;',
'use std::pin::Pin;',
'use std::task::{Context, Poll};',
'',
'use crate::io::AsyncWrite;',
'',
'impl AsyncWrite for Cursor<Vec<u8>> {',
' fn poll_write(self: Pin<&mut Self>, cx: &mut Context<\'_>, buf: &[u8]) -> Poll<usize> {',
' self.poll_flush(cx);',
' Poll::Ready(0)',
' }',
'',
' fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<\'_>) -> Poll<()> {',
' Poll::Ready(())',
' }',
'}',
'',
'impl AsyncWrite for Cursor<Box<[u8]>> {',
' fn poll_write(self: Pin<&mut Self>, cx: &mut Context<\'_>, buf: &[u8]) -> Poll<usize> {',
' self.poll_flush(cx);',
' Poll::Ready(0)',
' }',
'',
' fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<\'_>) -> Poll<()> {',
' Poll::Ready(())',
' }',
'}',
'',
].join('\n'),
// Through a parameter declared `Pin<&mut …>`.
'src/copy.rs': [
'use std::pin::Pin;',
'use std::task::{Context, Poll};',
'',
'use crate::file::File;',
'use crate::io::{AsyncRead, AsyncWrite};',
'',
'pub fn read_some<R: AsyncRead + ?Sized>(read: Pin<&mut R>, cx: &mut Context<\'_>, buf: &mut [u8]) -> Poll<usize> {',
' read.poll_read(cx, buf)',
'}',
'',
'pub fn vectored<T: AsyncWrite + ?Sized>(io: Pin<&mut T>) -> bool {',
' io.is_write_vectored()',
'}',
'',
'pub fn flush_file(file: Pin<&mut File>, cx: &mut Context<\'_>) -> Poll<()> {',
' file.poll_flush(cx)',
'}',
'',
].join('\n'),
'src/sleep.rs': [
'use std::pin::Pin;',
'',
'pub struct Sleep;',
'',
'impl Sleep {',
' pub fn reset(self: Pin<&mut Self>) {}',
'',
' pub fn deadline(&self) -> u64 {',
' 0',
' }',
'}',
'',
'pub fn rearm(sleep: Pin<&mut Sleep>) -> u64 {',
' let at = sleep.deadline();',
' sleep.reset();',
' at',
'}',
'',
].join('\n'),
'src/runtime.rs': [
'use std::rc::Rc;',
'',
'#[derive(Debug)]',
'pub struct Runtime;',
'',
'impl Clone for Runtime {',
' fn clone(&self) -> Self {',
' Runtime',
' }',
'}',
'',
'impl Runtime {',
' fn in_scope(&self) {}',
'',
' pub fn handle_event(self: Rc<Self>) {',
' let me = self.clone();',
' self.in_scope();',
' }',
'}',
'',
'pub struct Route;',
'',
'impl Route {',
' fn into_route(self: Box<Self>) -> Route {',
' *self',
' }',
'',
' fn call(self: Box<Self>) -> Route {',
' self.into_route()',
' }',
'}',
'',
].join('\n'),
'src/link.rs': [
'use std::ptr::NonNull;',
'',
'pub unsafe trait Link {',
' type Target;',
' unsafe fn from_raw(ptr: NonNull<Self::Target>) -> Self;',
'}',
'',
].join('\n'),
'src/net.rs': [
'use std::ptr::NonNull;',
'use std::sync::Arc;',
'',
'use crate::link::Link;',
'',
'pub struct ScheduledIo;',
'',
'unsafe impl Link for Arc<ScheduledIo> {',
' type Target = ScheduledIo;',
'',
' unsafe fn from_raw(ptr: NonNull<ScheduledIo>) -> Arc<ScheduledIo> {',
' unsafe { Arc::from_raw(ptr.as_ptr() as *const _) }',
' }',
'}',
'',
].join('\n'),
'src/wake.rs': [
'use std::ptr::NonNull;',
'use std::sync::Arc;',
'',
'use crate::link::Link;',
'use crate::net::ScheduledIo;',
'',
'pub unsafe fn wake(ptr: NonNull<ScheduledIo>) -> Arc<ScheduledIo> {',
' unsafe { Arc::from_raw(ptr.as_ptr()) }',
'}',
'',
].join('\n'),
// An inherent method and a trait's of one name: the inherent is the one called.
'src/grid.rs': [
'pub struct Grid {',
' cells: Vec<u8>,',
'}',
'',
'impl Grid {',
' pub fn len(&self) -> usize {',
' self.cells.len()',
' }',
'}',
'',
].join('\n'),
'src/shape.rs': [
'use crate::grid::Grid;',
'',
'pub trait Shape {',
' fn len(&self) -> usize;',
' fn area(&self) -> usize;',
'}',
'',
'impl Shape for Grid {',
' fn len(&self) -> usize {',
' 0',
' }',
'',
' fn area(&self) -> usize {',
' self.len()',
' }',
'}',
'',
'pub fn count(grid: &Grid) -> usize {',
' grid.len()',
'}',
'',
].join('\n'),
// A tuple's `serialize` is no named type's.
'src/ser.rs': [
'pub trait Serialize {',
' fn serialize(&self, out: &mut String);',
'}',
'',
'pub struct SocketAddr {',
' ip: u32,',
' port: u16,',
'}',
'',
'impl Serialize for SocketAddr {',
' fn serialize(&self, out: &mut String)',
' where',
' Self: Sized,',
' {',
' if out.is_empty() {',
' out.push(\'[\');',
' } else {',
' (self.ip, self.port).serialize(out)',
' }',
' }',
'}',
'',
'impl Serialize for Path {',
' fn serialize(&self, out: &mut String) {}',
'}',
'',
'pub struct Path;',
'',
].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 through `self: Pin<&mut Self>`', () => {
it('is the type\'s own method taking `self` that way, not a blanket impl on `Pin<P>`', () => {
expect(callsAt('src/file.rs', 32)).toEqual(['File::poll_flush@src/file.rs:25']);
});
it('in a trait\'s own body is the trait\'s method', () => {
expect(callsAt('src/io.rs', 8)).toEqual(['AsyncWrite::poll_write@src/io.rs:5']);
});
it('is the impl block\'s own method when one outside type has several impls', () => {
expect(callsAt('src/cursor.rs', 9)).toEqual(['Cursor::poll_flush@src/cursor.rs:13']);
expect(callsAt('src/cursor.rs', 20)).toEqual(['Cursor::poll_flush@src/cursor.rs:24']);
});
it('is `Pin`\'s own method before the type\'s namesake', () => {
expect(callsAt('src/file.rs', 27)).toEqual([]);
});
it('reaches the type\'s other methods through it', () => {
expect(callsAt('src/file.rs', 26)).toEqual(['File::complete@src/file.rs:15']);
});
});
describe('a Rust call on a value declared `Pin<&mut T>`', () => {
it('is never a blanket impl\'s method taking `self: Pin<&mut Self>`', () => {
expect(callsAt('src/copy.rs', 8)).toEqual([]);
});
it('keeps a blanket impl\'s `&self` method, which `Pin` answers first', () => {
expect(callsAt('src/copy.rs', 12)).toEqual(['Pin::is_write_vectored@src/io.rs:32']);
});
it('is the pinned type\'s own method', () => {
expect(callsAt('src/copy.rs', 16)).toEqual(['File::poll_flush@src/file.rs:25']);
expect(callsAt('src/sleep.rs', 14)).toEqual(['Sleep::deadline@src/sleep.rs:8']);
expect(callsAt('src/sleep.rs', 15)).toEqual(['Sleep::reset@src/sleep.rs:6']);
});
});
describe('a Rust call through `self: Rc<Self>` / `self: Box<Self>`', () => {
it('is the type\'s own method', () => {
expect(callsAt('src/runtime.rs', 17)).toEqual(['Runtime::in_scope@src/runtime.rs:13']);
expect(callsAt('src/runtime.rs', 29)).toEqual(['Route::into_route@src/runtime.rs:24']);
});
it('is the wrapper\'s own `clone`, not the type\'s', () => {
expect(callsAt('src/runtime.rs', 16)).toEqual([]);
});
it('is no method when it calls what a field holds, `(self.into_route)(state)`', () => {
expect(callsAt('src/erased.rs', 7)).toEqual([]);
expect(callsAt('src/erased.rs', 11)).toEqual(['MakeErased::into_route@src/erased.rs:6']);
});
});
describe('a Rust trait impl calling `self.<its own name>()`', () => {
it('hands the call on to the type\'s inherent method', () => {
expect(callsAt('src/measure.rs', 9)).toEqual(['Grid::len@src/grid.rs:6']);
});
it('links nothing for a type from outside the project, whose own method the index doesn\'t hold', () => {
expect(callsAt('src/waker.rs', 9)).toEqual([]);
});
it('calls itself again where the project\'s type has no such method', () => {
expect(callsAt('src/tester.rs', 17)).toEqual(['Tester::poll@src/tester.rs:12']);
});
});
describe('a Rust type\'s inherent method comes before a trait\'s of the same name', () => {
it('leaves std\'s inherent `Arc::from_raw` to std, with a project trait\'s in scope', () => {
expect(callsAt('src/net.rs', 12)).toEqual([]);
expect(callsAt('src/wake.rs', 8)).toEqual([]);
});
it('takes a project type\'s inherent method over its trait impl\'s', () => {
expect(callsAt('src/shape.rs', 14)).toEqual(['Grid::len@src/grid.rs:6']);
expect(callsAt('src/shape.rs', 19)).toEqual(['Grid::len@src/grid.rs:6']);
});
});
describe('a Rust method called on a tuple', () => {
it('is no named type\'s', () => {
expect(callsAt('src/ser.rs', 18)).toEqual([]);
});
});