fix(rust): a Self:: call is the function of the type whose impl it is in (#2700)

A Rust Self::x(...) call reached the resolver as a method call on a
receiver named Self, which names no type. So it fell to a guess by the
name alone, and linked only when nothing else in the project shared
that name. clap's Self::new(...) calls in impl Error, ValueParser and
the like stayed unresolved, and so did tokio's, axum's and serde's.
dioxus's Self::current() stopped resolving once its Android tooling's
AndroidTools::current was indexed.

Self is now the type of the impl (or trait) the call is written in. The
call reaches that type's function, or its variant for Self::Text(...),
in the type's own file first, then the nearest. As in Rust, the type's
inherent function comes first, with cfg twins standing for one another.
A trait impl's function is taken only when it is the only one, and
several trait impls of one name (From<A>, From<B>) are left unresolved.
Inside a trait impl, a call to the function's own name hands on to the
type's own function, so it never links to itself there. In the type's
own impl such a call is a recursion and stays one. A trait's own method
is the fallback only where nothing could shadow it.

A type from outside the project, such as axum's impl Listener for
tokio::net::TcpListener, has only the impls written beside the call.
When the type has no such function, the call stays unresolved instead
of going to another type's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-20 02:08:18 +00:00
committed by GitHub
parent 204315274e
commit 5530676f41
3 changed files with 320 additions and 0 deletions
+1
View File
@@ -211,6 +211,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In JavaScript and TypeScript, a barrel's `export { A, B as C } from './x'` now links each name it passes on to what `./x` exports, following barrels that pass it on in turn. Before, each name was looked up by itself and could link to a same-named symbol elsewhere in the project, such as another folder's `Provider`, and a name passed on from a package, like `export { debounce } from 'lodash'`, could link to a project function of that name. Re-index JavaScript and TypeScript projects after upgrading.
- In Swift, a call written after `!` or another prefix operator, or chained after `try` or `await`, now links to what it calls. `if !isReady()` links to `isReady`, `!store.contains(id)` and `try! JSONDecoder().decode(…)` link to the method of the type they're called on rather than to whichever `contains` or `decode` came first, and `!Settings.Defaults.isOn()` lands on the type it names. Re-index Swift projects after upgrading.
- In Swift, a call on a local created through `try`, `try!`, `try?` or `await`, or with a generic type's constructor, now links to that type's method: after `let storage = try! DiskStorage.Backend<String>(config: config)`, `storage.store(…)` links to `Backend`'s `store`. Before, such a local had no known type, so the call was left unlinked or matched by name to another type's method. Re-index Swift projects after upgrading.
- 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 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 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.
+209
View File
@@ -0,0 +1,209 @@
/**
* A Rust `Self::x(…)` call is `x` of the type `Self` is where it is written:
* `Self::new(…)` in `impl Error` is `Error::new`, `Self::Text(…)` in `impl
* Message` its variant, and in a trait's own body the trait's method. The
* receiver names no type of its own, so a guess by the name alone took such a
* call only when nothing else shared the name: clap's 118 `Self::new(…)`-style
* calls stayed unresolved, as did dioxus's `Self::current()` in `impl
* Workspace` once its Android tooling's `AndroidTools::current` was indexed.
*/
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-self-path-'));
const files: Record<string, string> = {
'Cargo.toml': '[package]\nname = "cli"\nversion = "0.1.0"\n',
'src/main.rs': 'mod android;\nmod bound;\nmod config;\nmod error;\nmod frame;\nmod listener;\nmod message;\nmod parse;\nmod parser;\nmod store;\nmod workspace;\n\nfn main() {}\n',
'src/error.rs': [
'pub struct Error {',
' kind: u8,',
'}',
'',
'impl Error {',
' pub fn new(kind: u8) -> Self {',
' Error { kind }',
' }',
'',
' pub fn io() -> Self {',
' Self::new(1)',
' }',
'}',
'',
].join('\n'),
'src/config.rs': [
'use crate::store::Store;',
'',
'pub struct Config;',
'',
'impl Config {',
' pub fn new() -> Self {',
' Config',
' }',
'}',
'',
'impl Store for Config {',
' fn open() -> Self {',
' Self::new()',
' }',
'}',
'',
].join('\n'),
'src/store.rs': [
'pub trait Store {',
' fn open() -> Self;',
'',
' fn open_default() -> Self',
' where',
' Self: Sized,',
' {',
' Self::open()',
' }',
'}',
'',
'pub fn open() {}',
'',
].join('\n'),
'src/message.rs': [
'pub enum Message {',
' Text(String),',
' Binary(Vec<u8>),',
'}',
'',
'impl Message {',
' pub fn text(s: &str) -> Self {',
' Self::Text(s.to_string())',
' }',
'}',
'',
].join('\n'),
'src/frame.rs': 'pub enum Frame {\n Text(String),\n}\n',
'src/workspace.rs': [
'pub struct Workspace;',
'',
'impl Workspace {',
' pub fn current() -> Workspace {',
' Workspace',
' }',
'',
' pub fn reload() -> Workspace {',
' Self::current()',
' }',
'',
' pub fn depth(n: u32) -> u32 {',
' if n == 0 { 0 } else { Self::depth(n - 1) + 1 }',
' }',
'}',
'',
].join('\n'),
'src/android.rs': 'pub struct AndroidTools;\n\nimpl AndroidTools {\n pub fn current() -> Option<AndroidTools> {\n None\n }\n}\n',
// `Self` is `Box<T>`: `Self::new` is `Box::new`, from outside the project,
// not the `new` of a struct that shares the trait's name.
'src/parse.rs': [
'pub trait Parser {',
' fn parse() -> Self;',
'}',
'',
'impl<T: Parser> Parser for Box<T> {',
' fn parse() -> Self {',
' Self::new(T::parse())',
' }',
'}',
'',
].join('\n'),
'src/parser.rs': 'pub struct Parser;\n\nimpl Parser {\n pub fn new() -> Self {\n Parser\n }\n}\n',
// An outside type's own `local_addr` is std's: nothing the index holds.
'src/bound.rs': [
'use std::net::{SocketAddr, TcpListener};',
'',
'pub trait Bound {',
' fn local_addr(&self) -> std::io::Result<SocketAddr>;',
'}',
'',
'impl Bound for TcpListener {',
' fn local_addr(&self) -> std::io::Result<SocketAddr> {',
' Self::local_addr(self)',
' }',
'}',
'',
].join('\n'),
// A trait impl's method handing on to the type's own function of its name.
'src/listener.rs': [
'pub trait Listener {',
' fn local_addr(&self) -> u16;',
'}',
'',
'pub struct TcpListener;',
'',
'impl TcpListener {',
' pub fn local_addr(&self) -> u16 {',
' 0',
' }',
'}',
'',
'impl Listener for TcpListener {',
' fn local_addr(&self) -> u16 {',
' Self::local_addr(self)',
' }',
'}',
'',
].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` edges out of `fn`, declared in `file` (at `line`, if given), reach, as `file@line`, sorted. */
const callsFrom = (file: string, fn: string, line?: number): string[] => {
const out: string[] = [];
for (const node of cg.getNodesInFile(file).filter((n) => n.name === fn && (line === undefined || n.startLine === line))) {
for (const edge of cg.getOutgoingEdges(node.id)) {
if (edge.kind !== 'calls') continue;
const target = cg.getNode(edge.target)!;
out.push(`${target.filePath}@${target.startLine}`);
}
}
return [...new Set(out)].sort();
};
describe('a Rust `Self::x(…)` call', () => {
it('is the impl\'s own type\'s function, whatever else shares its name', () => {
expect(callsFrom('src/error.rs', 'io')).toEqual(['src/error.rs@6']);
expect(callsFrom('src/workspace.rs', 'reload')).toEqual(['src/workspace.rs@4']);
// A recursion, in the type's own impl.
expect(callsFrom('src/workspace.rs', 'depth')).toEqual(['src/workspace.rs@12']);
});
it('is its variant, for a tuple variant built through `Self`', () => {
expect(callsFrom('src/message.rs', 'text')).toEqual(['src/message.rs@2']);
});
it('is the type\'s own in a trait impl, and the trait\'s in the trait\'s own body', () => {
expect(callsFrom('src/config.rs', 'open')).toEqual(['src/config.rs@6']);
expect(callsFrom('src/store.rs', 'open_default')).toEqual(['src/store.rs@2']);
});
it('is never another type\'s that shares the trait\'s name', () => {
expect(callsFrom('src/parse.rs', 'parse').filter((t) => t.startsWith('src/parser.rs'))).toEqual([]);
});
it('is the type\'s own function first, never the trait method it is written in', () => {
// The trait impl's `local_addr` (line 14) calls the type's own, at line 8.
expect(callsFrom('src/listener.rs', 'local_addr', 14)).toEqual(['src/listener.rs@8']);
expect(callsFrom('src/bound.rs', 'local_addr', 8)).toEqual([]);
});
});
+110
View File
@@ -8076,6 +8076,110 @@ function rustEnclosingImplLine(ref: UnresolvedRef, context: ResolutionContext):
return 0;
}
interface RustSelfType {
/** Its name (`TcpListener`), or the trait's in a trait's own body. */
type: string;
/** The first segment of the path it is written with (`tokio` of `tokio::net::TcpListener`), if any. */
root: string | null;
/** The trait a trait impl implements. */
trait: string | null;
/** Inside a trait's own body. */
isTrait: boolean;
}
/** The type `Self` is where `ref` is written: its `impl`'s implementing type (and trait), or the trait it sits in. */
function rustSelfType(ref: UnresolvedRef, context: ResolutionContext): RustSelfType | null {
const header = rustEnclosingImplLine(ref, context);
if (header === 0) return null;
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
let text = (lines[header - 1] ?? '').replace(/\/\/.*$/, '');
for (let j = header; !text.includes('{') && j < Math.min(lines.length, header + 8); j++) text += ` ${(lines[j] ?? '').replace(/\/\/.*$/, '')}`;
const trait = /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?trait\s+([A-Za-z_]\w*)/.exec(text)?.[1];
if (trait) return { type: trait, root: null, trait: null, isTrait: true };
const impl = parseRustImplHeader(text);
// The type as written: `tokio::net::TcpListener` after a trait impl's `for`,
// or right after an inherent impl's `impl<T: Into<U>>`.
const written = (impl
? /\sfor\s+(?:&\s*(?:'\w+\s+)?(?:mut\s+)?)?((?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*)/.exec(text)
: /\bimpl\b\s*(?:<(?:[^<>]|<[^<>]*>)*>)?\s*((?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*)/.exec(text))?.[1]?.split('::').map((s) => s.trim()) ?? [];
const type = impl ? impl.self : written[written.length - 1];
if (!type) return null;
return { type, root: written.length > 1 ? written[0]! : null, trait: impl?.trait || null, isTrait: false };
}
/**
* Whether `Self`'s type comes from outside the project: written through std's
* or a dependency's path, brought in by an outside `use`, or a std type the
* project doesn't declare. Its own functions are then nothing the index holds.
*/
function isRustOutsideSelf(self: RustSelfType, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (self.root !== null) return self.root === 'std' || self.root === 'core' || self.root === 'alloc' || rustDependencyCrates(context).has(self.root);
const uses = rustUsesOf(ref.filePath, context);
if (uses.external.has(self.type) && !uses.bound.has(self.type)) return true;
return RUST_STD_TYPES.has(self.type) && !isRustProjectNamed(self.type, RUST_DECLARED_TYPE_KINDS, context);
}
const RUST_SELF_PATH_KINDS = new Set(['method', 'function', 'enum_member']);
/**
* A Rust `Self::x(…)` call is `x` of the type `Self` is where it is written:
* `Self::new(…)` in `impl Error` is `Error::new`, `Self::Text(…)` in `impl
* Message` its variant, and in a trait impl or a trait's own body the trait's
* method when the type has none. The receiver names no type of its own, so a
* guess by the name alone took such a call only when nothing else shared the
* name: clap's 118 `Self::new(…)`-style calls stayed unresolved, tokio's 56.
* The type's own file first — where its impls are — then the nearest. Null
* when the type has no such function the index holds — the call is then no
* other type's — and undefined when there is no `impl` or `trait` around it.
*/
function matchRustSelfPath(name: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined {
const self = rustSelfType(ref, context);
if (!self) return undefined;
// An outside type (axum's `impl Listener for tokio::net::TcpListener`) has
// only the project's impls on it, written beside the call: another type of
// its name elsewhere is not it.
const outside = !self.isTrait && isRustOutsideSelf(self, ref, context);
// A trait's method is one its own body declares: clap's `impl<T: Parser>
// Parser for Box<T>` calls `Self::new`, which is `Box::new`, not the
// `new` of a struct also named `Parser`.
const inTrait = (n: Node, trait: string): boolean => (context.getNodesInFileNamed?.(n.filePath, trait) ?? context.getNodesInFile(n.filePath))
.some((t) => t.kind === 'trait' && t.name === trait && t.startLine <= n.startLine && (t.endLine ?? 0) >= n.startLine);
const pick = (owner: string, trait = false): Node | null => {
const all = context.getNodesByName(name).filter((n) => n.language === 'rust' && RUST_SELF_PATH_KINDS.has(n.kind) &&
(n.qualifiedName === `${owner}::${name}` || n.qualifiedName.endsWith(`::${owner}::${name}`)) && (!trait || inTrait(n, owner)) &&
(trait || !outside || n.filePath === ref.filePath) &&
// `fn local_addr(&self) { Self::local_addr(self) }` in a trait impl hands
// the call on to the type's own function of that name, never to itself;
// in the type's own impl it is a recursion (gleam's `resolve_type_var_id`).
(self.trait === null || n.id !== ref.fromNodeId));
// Rust looks up the type's own (inherent) function first, `#[cfg]` twins
// of it standing for one another; a trait impl's only when there is none,
// and several trait impls of one name (`From<A>`, `From<B>`) can't be told apart.
const inherent = all.filter((n) => !isRustTraitImplMethod(n, context));
const candidates = inherent.length > 0 ? inherent : all.length === 1 ? all : [];
if (candidates.length === 0) return null;
const own = candidates.find((n) => n.filePath === ref.filePath);
if (own) return own;
let best = candidates[0]!;
let score = computePathProximity(ref.filePath, best.filePath);
for (const c of candidates.slice(1)) {
const s = computePathProximity(ref.filePath, c.filePath);
if (s > score) {
score = s;
best = c;
}
}
return best;
};
// The trait's own method only where nothing could shadow it: an outside
// type may have an inherent one of the name, and `fn local_addr(&self) {
// Self::local_addr(self) }` hands the call on to the type's own.
const delegates = context.getNodeById?.(ref.fromNodeId)?.name === name;
const target = pick(self.type, self.isTrait) ?? (self.trait && !outside && !delegates ? pick(self.trait, true) : null);
if (!target) return null;
return { original: ref, targetNodeId: target.id, confidence: target.filePath === ref.filePath ? 0.9 : 0.8, resolvedBy: 'qualified-name' };
}
/** Whether `candidate` is declared in the same `impl` / `trait` block as `ref`, above it. */
function isInEnclosingRustImpl(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
const header = rustEnclosingImplLine(ref, context);
@@ -15450,6 +15554,12 @@ export function matchMethodCall(
if (literalMatch) return literalMatch;
}
// A Rust `Self::x(…)`: `x` of the impl's own type (matchRustSelfPath).
if (ref.language === 'rust' && match === colonMatch && objectOrClass === 'Self') {
const own = matchRustSelfPath(methodName!, ref, context);
if (own !== undefined) return own;
}
// Strategy 1: Direct class name match (existing logic). When the receiver
// names a class that exists in several files (`Logger.log()` / `Logger::log()`
// with a `Logger` in both `a/` and `b/`), try the class in the call site's