diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fd53c5b2..9c214f5db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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(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 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. diff --git a/__tests__/rust-self-path-calls.test.ts b/__tests__/rust-self-path-calls.test.ts new file mode 100644 index 000000000..bd96f1cde --- /dev/null +++ b/__tests__/rust-self-path-calls.test.ts @@ -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 = { + '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),', + '}', + '', + '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 {\n None\n }\n}\n', + // `Self` is `Box`: `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 Parser for Box {', + ' 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;', + '}', + '', + 'impl Bound for TcpListener {', + ' fn local_addr(&self) -> std::io::Result {', + ' 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([]); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 7cece60a9..f905764a0 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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>`. + 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 + // Parser for Box` 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`, `From`) 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