mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 08:47:04 +08:00
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:
@@ -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.
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user