diff --git a/CHANGELOG.md b/CHANGELOG.md index a32f5020..4bd5b8d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -236,6 +236,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Rust, a name written through a path, like `io::Error`, `std::ffi::OsStr`, `mio::Interest`, `ast::Pattern` or `Source::Record { … }`, now links to what that path names, or to nothing when the path leads into the standard library or another crate, and no longer to an item of the same name that merely sits in the same file. Before, clap's `std::ffi::OsStr` and `io::Error` linked to its own `OsStr` and `Error`, serde's tests' `Token::Struct { … }` to their own `Struct`, and gleam's `ast::Pattern` to the `Pattern` enum beside it. Re-index Rust projects after upgrading. - In Rust, a trait's associated type written as `::Name`, or bound in a trait's arguments like `Iterator`, now links only to what it can name: through `` the `impl`'s own, through a type that type's `impl` of the trait, and nothing through a generic parameter or from a binding. Before, axum's `::Io` linked to one listener's `type Io`, its `Iterator` to an unrelated `enum Item`, and dioxus's `Storage = …` bindings to a `Storage` trait. Re-index Rust projects after upgrading. - In Rust, a path through a module declared inline in the same file, like `inner::Cell` for a `mod inner { … }` above it, now finds the item however the code is indented, and no longer one in a module nested inside it. +- In Rust, a method call on a local variable or a parameter, like `opts.build()`, `server.run()` or `serializer.serialize_str(…)`, now links to the method of the type the variable is declared with: its parameter type or `let` annotation, the value it is built from (`Server::new()`, `Opts { … }`, `Arc::new(…)`), or the field a match arm takes from a variant (`Commands::Build(opts) => opts.build()`). A call through `&`, `Box`, `Rc` or `Arc` reaches the value inside, a call on an `impl Trait`, a `dyn Trait` or a bounded generic reaches the trait's method, and among types that share a name, the one the file imports wins. A call on a standard-library or dependency value, like `cx.waker()` on a `Context` or `path.join(…)` on a `PathBuf`, no longer links to an unrelated project method of the same name. Before, such calls were linked by the method's name alone, so dioxus's `opts.build()` lost its link once another type had a `build`. Re-index Rust projects after upgrading. Thanks @Egrn for the report and @danusha2345. (#2614) - 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. - In C++, a call on a parameter, a local lambda or functor, a lambda's parameter or a range-`for` variable no longer links to a function or method elsewhere that only shares its name, like protocolbuffers/protobuf's code generators calling `format(…)` on their `Formatter& format` parameter, which linked to the conformance suite's `TestResult::format`, or facebook/rocksdb's tests calling a local `auto key = [](int i) {…};`. C++ finds the local first, so that is what the call calls. diff --git a/__tests__/rust-typed-receivers.test.ts b/__tests__/rust-typed-receivers.test.ts new file mode 100644 index 00000000..7435e2c5 --- /dev/null +++ b/__tests__/rust-typed-receivers.test.ts @@ -0,0 +1,721 @@ +/** + * A Rust method call on a local or a parameter is a method of the type that + * name is declared with where the call is: a parameter's type, a `let`'s + * annotation or the value it is built from, a match arm's variant field. The + * method is that type's own (inherent) one first, then one of a trait impl + * the call's file has the trait of in scope, then a trait's provided one; + * the type is the one the file's `use`s name. Through `&`, `Box`, `Rc`, `Arc` + * a call reaches what they hold, and through `impl Trait` / `dyn Trait` the + * trait's method. A type from outside the project has no project method. The + * name strategies decide only where no declaration says what the name is: + * dioxus's `Commands::Build(opts) => opts.build()` lost its link once the + * build module's `BuildRequest::build` shared the method's name. + */ +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-typed-recv-')); + const files: Record = { + 'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nnetlib = "1"\n', + 'src/main.rs': [ + 'mod app;', + 'mod ast;', + 'mod canvas;', + 'mod cli;', + 'mod complete;', + 'mod disk;', + 'mod doc;', + 'mod doc_use;', + 'mod fallback;', + 'mod flags;', + 'mod form;', + 'mod http;', + 'mod io;', + 'mod layer;', + 'mod listener;', + 'mod net;', + 'mod once;', + 'mod overrides;', + 'mod paint;', + 'mod plan;', + 'mod render;', + 'mod report;', + 'mod request;', + 'mod runner;', + 'mod sched;', + 'mod serve;', + 'mod server;', + 'mod settings;', + 'mod shadow;', + 'mod start;', + 'mod store;', + 'mod sync;', + 'mod use_app;', + 'mod use_net;', + 'mod use_sync;', + 'mod widget;', + 'mod work;', + '', + 'fn main() {}', + '', + ].join('\n'), + 'src/once.rs': [ + 'use std::sync::Arc;', + '', + 'pub struct SetOnce;', + '', + 'impl SetOnce {', + ' pub fn new() -> Self {', + ' SetOnce', + ' }', + '', + ' pub fn set(&self, v: u8) {}', + '}', + '', + 'pub fn fill() {', + ' let once = Arc::new(SetOnce::new());', + ' once.set(1);', + '}', + '', + ].join('\n'), + 'src/form.rs': [ + 'use std::rc::Rc;', + '', + 'pub struct FormData;', + 'impl FormData {', + ' pub fn downcast(&self) -> Option<&u8> {', + ' None', + ' }', + '}', + '', + 'pub fn get(data: Rc) {', + ' data.downcast();', + '}', + '', + ].join('\n'), + 'src/sched.rs': [ + 'use std::sync::Arc;', + '', + 'pub trait Schedule {', + ' fn schedule(&self, task: u32);', + '}', + '', + 'pub struct Handle;', + '', + 'impl Handle {', + ' pub fn spawn(&self) {}', + '}', + '', + 'impl Schedule for Arc {', + ' fn schedule(&self, task: u32) {}', + '}', + '', + 'pub fn run(handle: Arc) {', + ' handle.schedule(1);', + ' handle.spawn();', + '}', + '', + ].join('\n'), + 'src/flags.rs': [ + 'pub struct ContextSeparator;', + 'impl ContextSeparator {', + ' pub fn into_bytes(self) {}', + '}', + 'pub struct FieldSeparator;', + 'impl FieldSeparator {', + ' pub fn into_bytes(self) {}', + '}', + '', + 'pub fn context() {', + ' use crate::flags::ContextSeparator as Separator;', + ' let bytes = |sep: Separator| sep.into_bytes();', + '}', + '', + 'pub fn field() {', + ' use crate::flags::FieldSeparator as Separator;', + ' let bytes = |sep: Separator| sep.into_bytes();', + '}', + '', + ].join('\n'), + 'src/doc.rs': [ + 'pub trait Documentable {', + ' fn to_doc(self) -> u8;', + '}', + '', + 'impl Documentable for netlib::EcoString {', + ' fn to_doc(self) -> u8 {', + ' 0', + ' }', + '}', + '', + 'impl Documentable for &netlib::EcoString {', + ' fn to_doc(self) -> u8 {', + ' 1', + ' }', + '}', + '', + ].join('\n'), + 'src/doc_use.rs': [ + 'use crate::doc::Documentable;', + 'use netlib::EcoString;', + '', + 'pub fn by_ref(string: &EcoString) {', + ' string.to_doc();', + '}', + '', + 'pub fn by_value(string: EcoString) {', + ' string.to_doc();', + '}', + '', + ].join('\n'), + 'src/runner.rs': [ + 'pub enum Outcome {', + ' Ignore { note: Option },', + ' Rebuild,', + '}', + '', + 'impl Outcome {', + ' pub fn escalate(self, other: Outcome) -> Outcome {', + ' other', + ' }', + '}', + '', + 'pub fn decide() {', + ' let mut result = Outcome::Ignore { note: None };', + ' result = result.escalate(Outcome::Rebuild);', + '}', + '', + ].join('\n'), + 'src/plan.rs': [ + 'pub struct Plan;', + '', + 'impl Plan {', + ' pub fn escalate(&self) {}', + '}', + '', + ].join('\n'), + 'src/ast.rs': [ + 'pub struct Statement {', + ' pub value: T,', + '}', + '', + 'pub type TypedStatement = Statement;', + 'pub type UntypedStatement = Statement;', + '', + 'impl TypedStatement {', + ' pub fn location(&self) {}', + '}', + '', + 'impl UntypedStatement {', + ' pub fn location(&self) {}', + '}', + '', + 'pub fn visit(statement: &TypedStatement) {', + ' statement.location();', + '}', + '', + 'impl TypedStatement {', + ' pub fn type_(&self) {}', + '}', + '', + 'pub enum Node {', + ' Leaf(Statement),', + '}', + '', + 'pub fn typed(node: &Node) {', + ' match node {', + ' Node::Leaf(statement) => statement.type_(),', + ' }', + '}', + '', + ].join('\n'), + 'src/io.rs': [ + 'pub trait HttpClient {', + ' fn send(&self);', + '}', + '', + ].join('\n'), + 'src/http.rs': [ + 'pub struct HttpClient;', + '', + 'impl HttpClient {', + ' pub fn new() -> Self {', + ' HttpClient', + ' }', + '}', + '', + 'impl crate::io::HttpClient for HttpClient {', + ' fn send(&self) {}', + '}', + '', + 'pub fn fetch() {', + ' let http = HttpClient::new();', + ' http.send();', + '}', + '', + ].join('\n'), + 'src/sync/mod.rs': 'pub mod batch;\npub mod semaphore;\n\npub use semaphore::Semaphore;\n', + 'src/sync/semaphore.rs': [ + 'pub struct Semaphore;', + '', + 'impl Semaphore {', + ' pub fn new() -> Self {', + ' Semaphore', + ' }', + '', + ' pub fn try_acquire_owned(&self) {}', + '}', + '', + ].join('\n'), + 'src/sync/batch.rs': [ + 'pub struct Semaphore;', + '', + 'impl Semaphore {', + ' pub fn new() -> Self {', + ' Semaphore', + ' }', + '}', + '', + ].join('\n'), + 'src/use_sync.rs': [ + 'use std::sync::Arc;', + '', + 'use crate::sync::Semaphore;', + '', + 'pub fn take() {', + ' let sem = Arc::new(Semaphore::new());', + ' sem.try_acquire_owned();', + '}', + '', + ].join('\n'), + 'src/listener.rs': [ + 'pub trait Listener {', + ' fn local_addr(&self) -> u16;', + '}', + '', + 'impl Listener for netlib::TcpListener {', + ' fn local_addr(&self) -> u16 {', + ' Self::local_addr(self)', + ' }', + '}', + '', + ].join('\n'), + 'src/serve.rs': [ + 'use crate::listener::Listener;', + 'use netlib::TcpListener;', + '', + 'pub fn addr(listener: TcpListener) {', + ' listener.local_addr();', + '}', + '', + ].join('\n'), + 'src/complete.rs': [ + 'pub struct ScratchRuntime;', + '', + 'impl netlib::Runtime for ScratchRuntime {', + ' fn complete(&self) {}', + '}', + '', + 'pub fn load_runtime() -> Box {', + ' Box::new(ScratchRuntime)', + '}', + '', + 'pub fn check() {', + ' let runtime = load_runtime();', + ' runtime.complete();', + '}', + '', + ].join('\n'), + 'src/cli.rs': [ + 'use crate::overrides::Overrides;', + '', + 'pub enum Commands {', + ' Build(Overrides),', + ' Tools(Tools),', + '}', + '', + 'pub enum Tools {', + ' Assets(AssetsArgs),', + '}', + '', + 'pub struct BuildArgs;', + '', + 'pub struct AssetsArgs;', + '', + 'impl AssetsArgs {', + ' pub fn run(self) {}', + '}', + '', + 'pub fn dispatch(args: Commands) {', + ' match args {', + ' Commands::Build(opts) => opts.build(),', + ' Commands::Tools(Tools::Assets(opts)) => opts.run(),', + ' }', + '}', + '', + ].join('\n'), + 'src/overrides.rs': [ + 'use crate::cli::BuildArgs;', + '', + 'pub struct Overrides {', + ' pub inner: T,', + '}', + '', + 'impl Overrides {', + ' pub fn build(self) {}', + '}', + '', + ].join('\n'), + 'src/request.rs': [ + 'pub struct BuildRequest;', + '', + 'impl BuildRequest {', + ' pub fn build(&self) {}', + '}', + '', + ].join('\n'), + 'src/server.rs': [ + 'pub struct Server;', + '', + 'impl Server {', + ' pub fn new() -> Self {', + ' Server', + ' }', + '', + ' pub fn run(&self) {}', + '}', + '', + ].join('\n'), + 'src/work.rs': [ + 'pub struct Worker;', + '', + 'impl Worker {', + ' pub fn run(&self) {}', + '}', + '', + ].join('\n'), + 'src/start.rs': [ + 'use std::rc::Rc;', + 'use std::sync::Arc;', + '', + 'use crate::server::Server;', + 'use crate::work::Worker;', + '', + 'pub fn shared(server: Arc) {', + ' server.run();', + '}', + '', + 'pub fn boxed(worker: Box) {', + ' worker.run();', + '}', + '', + 'pub fn counted(worker: &mut Rc) {', + ' worker.run();', + '}', + '', + "pub fn borrowed<'a>(srv: &'a Server) {", + ' srv.run();', + '}', + '', + 'pub fn pathed(w: crate::work::Worker) {', + ' w.run();', + '}', + '', + 'pub fn built() {', + ' let srv = Arc::new(Server::new());', + ' srv.run();', + '}', + '', + 'pub fn annotated() {', + ' let srv: Rc = Rc::new(Server::new());', + ' srv.run();', + '}', + '', + ].join('\n'), + 'src/app/mod.rs': 'pub mod config;\n', + 'src/app/config.rs': [ + 'pub struct Config;', + '', + 'impl Config {', + ' pub fn load(&self) {}', + '}', + '', + ].join('\n'), + 'src/net/mod.rs': 'pub mod config;\n', + 'src/net/config.rs': [ + 'pub struct Config;', + '', + 'impl Config {', + ' pub fn load(&self) {}', + '}', + '', + ].join('\n'), + 'src/use_app.rs': [ + 'use crate::app::config::Config;', + '', + 'pub fn read(cfg: Config) {', + ' cfg.load();', + '}', + '', + ].join('\n'), + 'src/use_net.rs': [ + 'use crate::net::config::Config;', + '', + 'pub fn read(cfg: &Config) {', + ' cfg.load();', + '}', + '', + ].join('\n'), + 'src/render.rs': [ + 'pub trait Render {', + ' fn draw(&self);', + '', + ' fn describe(&self) -> String {', + ' String::new()', + ' }', + '}', + '', + ].join('\n'), + 'src/paint.rs': [ + 'pub trait Paint {', + ' fn draw(&self);', + '}', + '', + ].join('\n'), + 'src/widget.rs': [ + 'use crate::paint::Paint;', + 'use crate::render::Render;', + '', + 'pub struct Widget;', + '', + 'impl Paint for Widget {', + ' fn draw(&self) {}', + '}', + '', + 'impl Render for Widget {', + ' fn draw(&self) {}', + '}', + '', + ].join('\n'), + 'src/report.rs': [ + 'pub struct Report;', + '', + 'impl Report {', + ' pub fn describe(&self) -> String {', + ' String::new()', + ' }', + '}', + '', + ].join('\n'), + 'src/canvas.rs': [ + 'use crate::render::Render;', + 'use crate::widget::Widget;', + '', + 'pub fn show(widget: &Widget) {', + ' widget.draw();', + ' widget.describe();', + '}', + '', + 'pub fn paint_all(r: impl Render, d: &dyn Render, b: Box) {', + ' r.describe();', + ' d.describe();', + ' b.describe();', + '}', + '', + ].join('\n'), + 'src/store.rs': [ + 'pub struct Store;', + '', + 'impl Store {', + ' pub fn sync_all(&self) {}', + '}', + '', + ].join('\n'), + 'src/disk.rs': [ + 'pub fn persist(file: std::fs::File) {', + ' file.sync_all();', + '}', + '', + ].join('\n'), + 'src/shadow.rs': [ + 'use crate::server::Server;', + 'use crate::work::Worker;', + '', + 'fn spare() -> Option {', + ' None', + '}', + '', + 'pub fn shadowed(srv: Server) {', + ' if let Some(srv) = spare() {', + ' srv.run();', + ' }', + '}', + '', + ].join('\n'), + 'src/settings.rs': [ + 'pub struct Settings;', + '', + 'impl Settings {', + ' pub fn combine(&self) {}', + '', + ' pub fn merge(&self, other: &Self) {', + ' other.combine();', + ' }', + '}', + '', + ].join('\n'), + 'src/layer.rs': [ + 'pub struct Layer;', + '', + 'impl Layer {', + ' pub fn combine(&self) {}', + '}', + '', + ].join('\n'), + 'src/fallback.rs': [ + 'pub struct Engine;', + '', + 'impl Engine {', + ' pub fn ignite(&self) {}', + '}', + '', + 'fn engine() -> Engine {', + ' Engine', + '}', + '', + 'pub fn go() {', + ' let e = engine();', + ' e.ignite();', + '}', + '', + ].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`, reach, as `file@line`, sorted. */ +const callsFrom = (file: string, fn: string): string[] => { + const out: string[] = []; + for (const node of cg.getNodesInFile(file).filter((n) => n.name === fn)) { + 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 method call on a declared local or parameter', () => { + it('is the method of the type a match arm\'s variant holds', () => { + // `Commands::Build(opts)` holds an `Overrides`, whatever else + // has a `build`; a nested variant's field the same way. + expect(callsFrom('src/cli.rs', 'dispatch')).toEqual(['src/cli.rs@17', 'src/overrides.rs@8']); + }); + + it('reaches what `&`, `Box`, `Rc` and `Arc` hold', () => { + expect(callsFrom('src/start.rs', 'shared')).toEqual(['src/server.rs@8']); + expect(callsFrom('src/start.rs', 'boxed')).toEqual(['src/work.rs@4']); + expect(callsFrom('src/start.rs', 'counted')).toEqual(['src/work.rs@4']); + expect(callsFrom('src/start.rs', 'borrowed')).toEqual(['src/server.rs@8']); + }); + + it('reads a type written through a path, or the value a `let` is built from', () => { + expect(callsFrom('src/start.rs', 'pathed')).toEqual(['src/work.rs@4']); + expect(callsFrom('src/start.rs', 'built')).toEqual(['src/server.rs@4', 'src/server.rs@8']); + expect(callsFrom('src/start.rs', 'annotated')).toEqual(['src/server.rs@4', 'src/server.rs@8']); + }); + + it('is the method of the type the file\'s `use` names, among types sharing its name', () => { + expect(callsFrom('src/use_app.rs', 'read')).toEqual(['src/app/config.rs@4']); + expect(callsFrom('src/use_net.rs', 'read')).toEqual(['src/net/config.rs@4']); + }); + + it('is the impl of the trait in scope, then the trait\'s provided method', () => { + expect(callsFrom('src/canvas.rs', 'show')).toEqual(['src/render.rs@4', 'src/widget.rs@11']); + }); + + it('is the trait\'s method through `impl Trait`, `&dyn Trait` and `Box`', () => { + expect(callsFrom('src/canvas.rs', 'paint_all')).toEqual(['src/render.rs@4']); + }); + + it('is the impl\'s own type\'s method through `Self`', () => { + expect(callsFrom('src/settings.rs', 'merge')).toEqual(['src/settings.rs@4']); + }); + + it('is no project method for a type from outside the project', () => { + expect(callsFrom('src/disk.rs', 'persist')).toEqual([]); + }); + + it('never takes the type of a declaration an inner binding shadows', () => { + expect(callsFrom('src/shadow.rs', 'shadowed').filter((t) => t.startsWith('src/server.rs'))).toEqual([]); + }); + + it('is left to the name strategies where no declaration writes the type', () => { + expect(callsFrom('src/fallback.rs', 'go')).toEqual(['src/fallback.rs@4', 'src/fallback.rs@7']); + // Known only by a trait from outside the project, a value is any + // implementation's, the project's among them. + expect(callsFrom('src/complete.rs', 'check')).toEqual(['src/complete.rs@4', 'src/complete.rs@7']); + }); + + it('is the enum\'s method for a value a struct variant\'s literal builds', () => { + expect(callsFrom('src/runner.rs', 'decide')).toEqual(['src/runner.rs@7']); + }); + + it('is the method an impl of a type alias declares, by the alias\'s name', () => { + expect(callsFrom('src/ast.rs', 'visit')).toEqual(['src/ast.rs@9']); + // A `Statement` a generic enum's variant holds has the methods an + // impl of its one alias with them declares. + expect(callsFrom('src/ast.rs', 'typed')).toEqual(['src/ast.rs@21']); + }); + + it('is the impl\'s method, not the declaration of a trait sharing the type\'s name', () => { + expect(callsFrom('src/http.rs', 'fetch')).toEqual(['src/http.rs@10', 'src/http.rs@4']); + }); + + it('is the method of the one type, among those the path may name, that has it', () => { + expect(callsFrom('src/use_sync.rs', 'take')).toContain('src/sync/semaphore.rs@8'); + }); + + it('is no project trait\'s method that hands the call on to the outside type\'s own', () => { + expect(callsFrom('src/serve.rs', 'addr')).toEqual([]); + }); + + it('reaches through an `Arc` a method only `Pin` has of its own', () => { + expect(callsFrom('src/once.rs', 'fill')).toEqual(['src/once.rs@10', 'src/once.rs@6']); + }); + + it('is a project impl on the `Arc` itself before what the `Arc` holds', () => { + expect(callsFrom('src/sched.rs', 'run')).toEqual(['src/sched.rs@10', 'src/sched.rs@14']); + }); + + it('reaches the held type\'s `downcast` through an `Rc`, which only an `Rc` has', () => { + expect(callsFrom('src/form.rs', 'get')).toEqual(['src/form.rs@5']); + }); + + it('is the type a `use` in the calling block names', () => { + expect(callsFrom('src/flags.rs', 'context')).toEqual(['src/flags.rs@3']); + expect(callsFrom('src/flags.rs', 'field')).toEqual(['src/flags.rs@7']); + }); + + it('is the impl for a reference on a reference, for a value on a value', () => { + expect(callsFrom('src/doc_use.rs', 'by_ref')).toEqual(['src/doc.rs@12']); + expect(callsFrom('src/doc_use.rs', 'by_value')).toEqual(['src/doc.rs@6']); + }); +}); diff --git a/__tests__/rust-wrapped-self-calls.test.ts b/__tests__/rust-wrapped-self-calls.test.ts index 09745545..4e204b6f 100644 --- a/__tests__/rust-wrapped-self-calls.test.ts +++ b/__tests__/rust-wrapped-self-calls.test.ts @@ -450,8 +450,8 @@ describe('a Rust call through `self: Pin<&mut Self>`', () => { }); 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('is the bound\'s method, never a blanket impl\'s taking `self: Pin<&mut Self>`', () => { + expect(callsAt('src/copy.rs', 8)).toEqual(['AsyncRead::poll_read@src/io.rs:16']); }); it('keeps a blanket impl\'s `&self` method, which `Pin` answers first', () => { diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 8eaabb5c..fb9c4e87 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -23,6 +23,7 @@ import { isCFeatureVariant, isOffByDefault } from './c-feature-variants'; import { isTestPath } from '../search/query-utils'; import { isMinifiedContent } from '../extraction/generated-detection'; import { getCargoWorkspaceCrateMap } from './frameworks/cargo-workspace'; +import { readRustScopes, rustBindingAt, rustInnerBlockAt, rustBoundsOf, rustFnAt, rustImplAt, splitRustTopLevel, type RustBinding, type RustPatternSite, type RustScopes } from './rust-scopes'; /** * Ceiling on how many same-named definitions a FUZZY name-match strategy will * score. A name defined more times than this is "ubiquitous" — a method/symbol @@ -3326,6 +3327,10 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex if (lang === 'rust') { if (candidate.visibility !== 'private') return true; if (isRustTraitImplMethod(candidate, context)) return true; + // A method a trait declares carries no `pub` of its own: it is the + // trait's, reached through a value whose type the call's receiver + // declares (matchRustTypedReceiverCall). A guess by its name alone stays out. + if (candidate.kind === 'method' && RUST_TYPED_REFS.get(context)?.has(ref) && rustTraitOf(candidate, context)?.visibility !== 'private') return true; const owner = rustModuleDir(candidate.filePath); return ref.filePath.startsWith(owner + '/'); } @@ -8899,13 +8904,23 @@ const RUST_POINTER_TRAIT_METHODS = 'clone clone_from as_ref borrow deref fmt eq * methods, and the std traits it implements (`Rc`'s `clone`, `Deref`, * `Debug`). tokio's `self.get_mut()` through `self: Pin<&mut Self>` is * `Pin::get_mut`, not its own `BufReader::get_mut`. (`Pin<&mut T>` is no - * `Clone`: `self.clone()` through one is the type's.) + * `Clone`: `self.clone()` through one is the type's.) A lock guard or a + * `RefCell`'s `Ref` answers only `Deref` and `Debug`: its `clone()` clones + * what it guards. `downcast` is only a `Box`'s or an `Rc`'s: dioxus's `data.downcast()` on an `Rc` is `FormData`'s. */ const RUST_WRAPPER_OWN_METHODS = rustNameSets({ Pin: `${RUST_PIN_METHODS} deref deref_mut fmt eq ne partial_cmp lt le gt ge cmp max min clamp hash into try_into to_string`, - Box: `${RUST_POINTER_TRAIT_METHODS} as_mut borrow_mut deref_mut downcast`, - Rc: `${RUST_POINTER_TRAIT_METHODS} downcast`, - Arc: `${RUST_POINTER_TRAIT_METHODS} downcast`, + Box: `${RUST_POINTER_TRAIT_METHODS} as_mut borrow_mut deref_mut`, + Rc: RUST_POINTER_TRAIT_METHODS, + Arc: RUST_POINTER_TRAIT_METHODS, + ManuallyDrop: `${RUST_POINTER_TRAIT_METHODS} deref_mut`, + Cow: `${RUST_POINTER_TRAIT_METHODS} into_owned to_mut is_borrowed is_owned`, + MutexGuard: 'deref deref_mut fmt', + RwLockReadGuard: 'deref fmt', + RwLockWriteGuard: 'deref deref_mut fmt', + Ref: 'deref fmt', + RefMut: 'deref deref_mut fmt', }); /** @@ -9325,6 +9340,1224 @@ function isRustTypeParam(name: string, ref: UnresolvedRef, context: ResolutionCo }); } +// A Rust method call on a local or a parameter — `opts.build()` — is a call +// on the type that name is declared with where the call is (the binding +// rust-scopes.ts reads), resolved as Rust resolves it on that type. + +const RUST_SCOPES = new WeakMap>(); + +/** + * A Rust file read for its bindings (readRustScopes), for the files used + * last: calls arrive file by file, and each looks into the files declaring + * what it reaches. + */ +function rustScopesOf(filePath: string, context: ResolutionContext): RustScopes | null { + let memo = RUST_SCOPES.get(context); + if (!memo) RUST_SCOPES.set(context, (memo = new Map())); + const hit = memo.get(filePath); + if (hit !== undefined) { + memo.delete(filePath); + memo.set(filePath, hit); + return hit; + } + const source = context.readFile(filePath); + const scopes = source === null ? null : readRustScopes(source); + if (memo.size >= 64) memo.delete(memo.keys().next().value!); + memo.set(filePath, scopes); + return scopes; +} + +/** The 1-based line of an offset in a file read by rustScopesOf. */ +function rustLineAt(scopes: RustScopes, offset: number): number { + let lo = 0; + let hi = scopes.lineStarts.length - 1; + while (lo < hi) { + const mid = (lo + hi + 1) >> 1; + if (scopes.lineStarts[mid]! <= offset) lo = mid; + else hi = mid - 1; + } + return lo + 1; +} + +/** + * A Rust type as some declaration writes it: its text, and the file and + * offset it is written at, whose `use`s, generics and `impl` say what its + * names are. `self` is the type `Self` names in a type's own declaration (an + * enum's variant), `generics` that declaration's parameters; `wrappers` the + * `Box`, `Rc`, `Arc`… the value is held in, which a call reaches through. + */ +interface RustWrittenType { + text: string; + file: string; + at: number; + self?: string; + generics?: string; + wrappers?: string[]; +} + +/** + * What a Rust type is, for calling a method on it, and the wrappers + * (`MutexGuard<'_, T>`) a call goes through to reach it, outermost first. + */ +type RustTypeRead = ( + /** A type by its path: `crate::work::Worker`, `Overrides`. */ + | { kind: 'named'; name: string; path: string[]; args: string } + /** A trait object, an `impl Trait` or a bounded generic: the traits it is known by. */ + | { kind: 'traits'; traits: string[] } + /** A type the project doesn't declare: std's, a dependency's, a primitive, a tuple, a pointer. */ + | { kind: 'outside'; name: string } +) & { wrappers: string[]; byRef: boolean; written: RustWrittenType } | { kind: 'unread' }; + +/** Types that hand a method call on to what they hold (they `Deref` to it) when the method is not their own. */ +const RUST_DEREF_WRAPPERS: ReadonlySet = new Set([ + 'Box', 'Rc', 'Arc', 'Pin', 'Cow', 'MutexGuard', 'RwLockReadGuard', 'RwLockWriteGuard', 'Ref', 'RefMut', 'ManuallyDrop', +]); +/** The std wrappers a method's `self` can be declared through (rustSelfWrapper). */ +const RUST_SELF_WRAPPERS: ReadonlySet = new Set(['Pin', 'Box', 'Rc', 'Arc']); +/** Traits a bound or a trait object may name that give it no methods. */ +const RUST_MARKER_TRAITS: ReadonlySet = new Set(['Send', 'Sync', 'Unpin', 'Sized', 'Copy', 'UnwindSafe', 'RefUnwindSafe']); +const RUST_PRIMITIVE_TYPES: ReadonlySet = new Set([ + 'bool', 'char', 'str', 'u8', 'u16', 'u32', 'u64', 'u128', 'usize', 'i8', 'i16', 'i32', 'i64', 'i128', 'isize', 'f32', 'f64', +]); +const RUST_OWNER_KINDS: ReadonlySet = new Set(['struct', 'enum', 'union']); + +/** The offset of the `>` closing the `<` at `open` in a type's text, or -1. */ +function rustAngleEnd(text: string, open: number): number { + let depth = 0; + for (let i = open; i < text.length; i++) { + const c = text[i]; + if (c === '<') depth++; + else if (c === '>' && text[i - 1] !== '-' && --depth === 0) return i; + } + return -1; +} + +/** A type's path: `crate::work::Worker` → segments `crate`, `work` and name `Worker`; `Overrides` its arguments too. */ +function rustPathHead(text: string): { segments: string[]; name: string; args: string } | null { + let t = text.trim().replace(/^::\s*/, ''); + let args = ''; + const open = t.indexOf('<'); + if (open >= 0) { + if (rustAngleEnd(t, open) !== t.length - 1) return null; + args = t.slice(open + 1, -1).trim(); + t = t.slice(0, open).trim().replace(/\s*::\s*$/, ''); + } + const parts = t.split('::').map((s) => s.trim()); + if (!parts.every((p) => /^[A-Za-z_]\w*$/.test(p))) return null; + const name = parts.pop()!; + return { segments: parts, name, args }; +} + +/** A `{…}` or `(…)` that closes only at the text's end. */ +function rustGroupIsWhole(text: string): boolean { + let depth = 0; + for (let i = 0; i < text.length; i++) { + const c = text[i]; + if (c === '(' || c === '[' || c === '{') depth++; + else if (c === ')' || c === ']' || c === '}') { + if (--depth === 0) return i === text.length - 1; + } + } + return false; +} + +/** A value that ends in a call — `Foo::new(a)`, `x.unwrap()` — as its callee's text and arguments. */ +function rustTrailingCall(value: string): { callee: string; args: string } | null { + if (!value.endsWith(')')) return null; + let depth = 0; + for (let i = value.length - 1; i >= 0; i--) { + const c = value[i]; + if (c === ')' || c === ']' || c === '}') depth++; + else if (c === '(' || c === '[' || c === '{') { + if (--depth === 0) { + if (c !== '(') return null; + // `Vec::::new()`: the turbofish is the callee's. + return { callee: value.slice(0, i).trim(), args: value.slice(i + 1, -1) }; + } + } + } + return null; +} + +/** The text after a Rust function signature's `->`: `(…) -> Result` → `Result`. */ +function rustReturnText(signature: string | undefined): string | null { + if (!signature?.startsWith('(')) return null; + let depth = 0; + for (let i = 0; i < signature.length; i++) { + const c = signature[i]; + if (c === '(') depth++; + else if (c === ')' && --depth === 0) { + const ret = /^\s*->\s*([\s\S]+)$/.exec(signature.slice(i + 1))?.[1]?.trim(); + return ret || null; + } + } + return null; +} + +/** What `times` `?`s / `.unwrap()`s take a value of the written type to: the `T` of `Result` or `Option`. */ +function rustUnwrapped(written: RustWrittenType, times: number): RustWrittenType | null { + let text = written.text; + for (let i = 0; i < times; i++) { + const head = rustPathHead(text); + if (!head || (head.name !== 'Result' && head.name !== 'Option')) return null; + const first = splitRustTopLevel(head.args, ',')[0]; + if (!first) return null; + text = first; + } + return { ...written, text }; +} + +/** A path a Rust `use` binds a name to, and the block it is written in (file-level: from -1). */ +interface RustUseBinding { + path: string[]; + from: number; + to: number; +} + +const RUST_USE_PATHS = new WeakMap>>(); + +/** + * The names a Rust file's `use`s bind, each to the path it binds, the item's + * own name last: `use crate::net::config::Config as NetConfig;` binds + * `NetConfig` to `crate::net::config::Config`, `use std::io::{self, Read};` + * `io` to `std::io`. A glob binds no name. A `use` in a block binds only + * there: ripgrep's flag definitions each `use … as Separator` a type of + * their own. + */ +function rustUseBindings(filePath: string, context: ResolutionContext): Map { + let memo = RUST_USE_PATHS.get(context); + if (!memo) RUST_USE_PATHS.set(context, (memo = new Map())); + const hit = memo.get(filePath); + if (hit) return hit; + const bindings = new Map(); + const scopes = rustScopesOf(filePath, context); + for (const m of (scopes?.code ?? '').matchAll(/(?:^|[;{}\s])use\s+([^;]{1,2000});/g)) { + const bound = new Map(); + expandRustUse(m[1]!, [], bound); + const block = scopes ? rustInnerBlockAt(scopes, m.index!) : null; + for (const [name, path] of bound) { + const list = bindings.get(name) ?? []; + list.push({ path, from: block ? block[0] : -1, to: block ? block[1] : Infinity }); + bindings.set(name, list); + } + } + if (memo.size >= 256) memo.delete(memo.keys().next().value!); + memo.set(filePath, bindings); + return bindings; +} + +/** The path a `use` in scope at `at` binds `name` to — the innermost block's first — or a file-level `use`'s without `at`. */ +function rustUsePath(name: string, filePath: string, at: number | undefined, context: ResolutionContext): string[] | undefined { + let best: RustUseBinding | undefined; + for (const b of rustUseBindings(filePath, context).get(name) ?? []) { + const visible = at === undefined ? b.from < 0 : at > b.from && at < b.to; + if (visible && (!best || b.from > best.from)) best = b; + } + return best?.path; +} + +function expandRustUse(tree: string, prefix: string[], bound: Map): void { + const t = tree.trim().replace(/^::\s*/, ''); + if (t === '') return; + const open = t.indexOf('{'); + if (open >= 0 && t.endsWith('}')) { + const head = t.slice(0, open).replace(/::\s*$/, '').trim(); + const within = [...prefix, ...(head === '' ? [] : head.split('::').map((s) => s.trim()))]; + let depth = 0; + let start = open + 1; + for (let i = open + 1; i < t.length - 1; i++) { + const c = t[i]; + if (c === '{') depth++; + else if (c === '}') depth--; + else if (c === ',' && depth === 0) { + expandRustUse(t.slice(start, i), within, bound); + start = i + 1; + } + } + expandRustUse(t.slice(start, t.length - 1), within, bound); + return; + } + const m = /^((?:[A-Za-z_]\w*\s*::\s*)*)([A-Za-z_]\w*|\*)(?:\s+as\s+([A-Za-z_]\w*))?$/.exec(t); + if (!m || m[2] === '*' || m[3] === '_') return; + const segments = [...prefix, ...(m[1] ? m[1].split('::').map((s) => s.trim()).filter(Boolean) : [])]; + if (m[2] === 'self') { + const name = m[3] ?? segments[segments.length - 1]; + if (name) bound.set(name, segments); + return; + } + bound.set(m[3] ?? m[2]!, [...segments, m[2]!]); +} + +/** The module path of a Rust file in its crate: `crates/cli/src/net/config.rs` → `net`, `config`; a `lib.rs` none. */ +function rustModulePath(filePath: string): string[] { + const parts = filePath.split('/'); + let at = -1; + for (let i = parts.length - 2; i >= 0; i--) { + if (parts[i] === 'src' || parts[i] === 'tests' || parts[i] === 'examples' || parts[i] === 'benches') { + at = i; + break; + } + } + const modules = parts.slice(at + 1); + const file = modules.pop()!.replace(/\.rs$/, ''); + if (file !== 'mod' && file !== 'lib' && file !== 'main') modules.push(file); + return modules; +} + +/** + * Whether the modules a path walks (`net::config`) are, in order, among those + * of the file declaring `n`: a path may name a type through a module that + * re-exports it (`tokio::sync::Mutex` is `sync/mutex.rs`'s). + */ +function rustModulesLead(modules: string[], n: Node): boolean { + let k = 0; + for (const m of rustModulePath(n.filePath)) if (k < modules.length && m === modules[k]) k++; + return k === modules.length; +} + +const RUST_WALKED_ITEMS = new WeakMap | null>>(); + +/** + * The ids of the items a Rust path names from the module around `at` in + * `file` (rustUsePathItems); null where the project's modules can't be + * walked along it. + */ +function rustWalkedItems(segs: string[], file: string, at: number | undefined, context: ResolutionContext): Set | null { + const from = rustModuleAt(file, at ?? 0, context); + let memo = RUST_WALKED_ITEMS.get(context); + if (!memo) RUST_WALKED_ITEMS.set(context, (memo = new Map())); + const key = `${file}\0${from.block?.open ?? -1}\0${segs.join('::')}`; + const hit = memo.get(key); + if (hit !== undefined) return hit; + const items = rustUsePathItems(segs, from, context, new Set()); + const ids = items === null ? null : new Set(items.map((n) => n.id)); + memo.set(key, ids); + return ids; +} + +/** The project types a Rust type name can mean where it is written. */ +interface RustTypeLookup { + /** Its declarations the name can mean there. */ + nodes: Node[]; + /** It names a type from outside the project: std's, a dependency's, a primitive. */ + outside: boolean; + /** No project type has the name: a dependency's, or one a macro declares, whose impls the index may hold. */ + undeclared?: boolean; + /** The name, if a `use … as` renamed it. */ + name: string; +} + +/** + * The type a name written in `file` — bare, or through `path` — means there: + * the one a `use` brings in by that path, the file's own, one a glob brings + * in. std's, a dependency's or a primitive is outside the project. + */ +function rustTypeLookup(name: string, writtenPath: string[], file: string, context: ResolutionContext, at?: number): RustTypeLookup { + let segments = writtenPath; + let real = name; + let viaUse = false; + if (segments.length === 0) { + const bound = rustUsePath(name, file, at, context); + if (bound && bound.length > 1) { + real = bound[bound.length - 1]!; + segments = bound.slice(0, -1); + viaUse = true; + } + } else { + // `io::Error` under `use std::io;`: the path starts at what a `use` binds. + const head = rustUsePath(segments[0]!, file, at, context); + if (head && head.length > 0) segments = [...head, ...segments.slice(1)]; + } + const outside: RustTypeLookup = { nodes: [], outside: true, name: real }; + const root = segments[0]; + const crates = rustProjectCrates(context); + if (root === 'std' || root === 'core' || root === 'alloc') return outside; + if (root !== undefined && !crates.has(root) && rustDependencyCrates(context).has(root)) return outside; + if (segments.length === 0 && RUST_PRIMITIVE_TYPES.has(real)) return outside; + const fileRef = { filePath: file } as UnresolvedRef; + const all = context.getNodesByName(real).filter((n) => n.language === 'rust' && n.name === real && RUST_DECLARED_TYPE_KINDS.has(n.kind)); + if (all.length === 0) { + if (segments.length === 0 && (RUST_STD_TYPES.has(real) || RUST_PRELUDE.has(real))) return outside; + return { nodes: [], outside: false, undeclared: true, name: real }; + } + let nodes = all.filter((n) => isRustItemVisibleFrom(n, fileRef)); + // A path the project's modules can be walked along, written or the one a + // `use` binds, names the item of the module it walks to, as a `use` does in + // isRustNameInScope: tokio's `crate::runtime::Handle` is the one + // `runtime/mod.rs` re-exports. The modules' names decide where it can't. + if (segments.length > 0) { + const walked = rustWalkedItems([...segments, real], file, at, context); + const named = walked === null ? [] : nodes.filter((n) => walked.has(n.id)); + if (named.length > 0) return { nodes: named, outside: false, name: real }; + } + const modules = segments.filter((s) => s !== 'crate' && s !== 'self' && s !== 'super' && !crates.has(s)); + if (modules.length > 0) { + let within = nodes.filter((n) => rustModulesLead(modules, n)); + // Through a path from a crate's root, the nearest module re-exports it: + // `tokio::runtime::Handle` is `runtime/handle.rs`'s, not a scheduler's + // `Handle` deeper down. + if (within.length > 1 && root !== undefined && (root === 'crate' || crates.has(root))) { + const depth = (n: Node): number => rustModulePath(n.filePath).length; + const nearest = Math.min(...within.map(depth)); + within = within.filter((n) => depth(n) === nearest); + } + if (within.length > 0) nodes = within; + } + if (root !== undefined) nodes = nodes.filter((n) => rustUseCanReach(new Set([root]), n, fileRef, context)); + if (segments.length === 0 && !viaUse) { + const same = nodes.filter((n) => n.filePath === file); + if (same.length > 0) return { nodes: same, outside: false, name: real }; + const u = rustUsesOf(file, context); + if (u.external.has(real) && !u.bound.has(real)) return outside; + // What the file's globs bring in, by the modules they walk to where those + // can be followed (rustGlobScope): dioxus's autofmt reads `AttributeValue` + // through `use dioxus_rsx::*`. + const globbed = nodes.filter((n) => rustGlobScope(real, n, u, fileRef, context)); + if (globbed.length > 0) return { nodes: globbed, outside: false, name: real }; + if (RUST_STD_TYPES.has(real) || RUST_PRELUDE.has(real)) return outside; + // In scope some way the file doesn't write — a parent module's glob, a + // macro's `use`: a type of its own module tree, or of its directory, or + // else any (the method then tells them apart, rustTypedMethodCall). + const dir = path.posix.dirname(file); + const near = nodes.filter((n) => file.startsWith(`${rustModuleDir(n.filePath)}/`) || path.posix.dirname(n.filePath) === dir); + if (near.length > 0) nodes = near; + } + return { nodes, outside: false, name: real }; +} + +/** + * What a Rust type as written is, for a method call on it: the type it names + * through `&`, `Box`, `Rc`, `Arc`, `Pin`… (its `wrappers`); the traits a + * trait object, an `impl Trait` or a bounded generic is known by; `Self` the + * type of the `impl` (or trait) it is written in. + */ +function rustReadType(written: RustWrittenType, ref: UnresolvedRef, context: ResolutionContext): RustTypeRead { + const wrappers: string[] = [...(written.wrappers ?? [])]; + const byRef = written.text.trim().startsWith('&'); + let t = written.text.trim(); + for (let i = 0; i < 8; i++) { + const before = t; + t = t.replace(/^&\s*(?:'\w+\s+)?(?:mut\s+)?/, '').replace(/^mut\s+/, '').trim(); + const wrapper = /^(?:::\s*)?(?:(?:std|alloc|core)\s*::\s*(?:\w+\s*::\s*)*)?([A-Za-z_]\w*)\s* !/^['?]/.test(b) && !RUST_MARKER_TRAITS.has(rustPathHead(b)?.name ?? '')); + return traits.length === 0 ? { kind: 'outside', name: object[1]!, wrappers, byRef, written } : { kind: 'traits', traits, wrappers, byRef, written }; + } + // A tuple, an array or a slice, a pointer, a function: nothing the project declares. + if (/^[([*]/.test(t) || /^(?:unsafe\s+|extern\s+"[^"]*"\s+)*fn\s*\(/.test(t)) return { kind: 'outside', name: t[0]!, wrappers, byRef, written }; + const head = rustPathHead(t); + if (!head) return { kind: 'unread' }; + const { segments, name, args } = head; + // `Self::Item`, `T::Output`: an associated type. + if (segments.length > 0 && (segments[0] === 'Self' || rustGenericBounds(segments[0]!, written, context) !== undefined)) return { kind: 'unread' }; + if (segments.length === 0 && name === 'Self') { + if (written.self) return { kind: 'named', name: written.self, path: [], args: '', wrappers, byRef, written }; + const scopes = rustScopesOf(written.file, context); + const self = rustSelfType({ ...ref, filePath: written.file, line: scopes ? rustLineAt(scopes, written.at) : ref.line }, context); + if (!self) return { kind: 'unread' }; + if (self.isTrait) return { kind: 'traits', traits: [self.type], wrappers, byRef, written }; + return { kind: 'named', name: self.type, path: self.root ? [self.root] : [], args: '', wrappers, byRef, written }; + } + if (segments.length === 0) { + const bounds = rustGenericBounds(name, written, context); + if (bounds !== undefined) { + const traits = bounds.filter((b) => !RUST_MARKER_TRAITS.has(rustPathHead(b)?.name ?? '')); + return traits.length === 0 ? { kind: 'unread' } : { kind: 'traits', traits, wrappers, byRef, written }; + } + if (RUST_PRIMITIVE_TYPES.has(name)) return { kind: 'outside', name, wrappers, byRef, written }; + } + return { kind: 'named', name, path: segments, args, wrappers, byRef, written }; +} + +/** + * The bounds of a generic parameter `name` where a type is written: those of + * the function and the `impl` or trait around it. Empty for a declaration's + * own parameter (an enum's `T`), which is whatever a use of it makes it; + * undefined when `name` is no generic parameter there. + */ +function rustGenericBounds(name: string, written: RustWrittenType, context: ResolutionContext): string[] | undefined { + if (written.generics !== undefined) return rustBoundsOf(name, [{ generics: written.generics, where: '' }]) === undefined ? undefined : []; + const scopes = rustScopesOf(written.file, context); + if (!scopes) return undefined; + const items: Array<{ generics: string; where: string }> = []; + const fn = rustFnAt(scopes, written.at); + if (fn) items.push(fn); + const block = rustImplAt(scopes, written.at); + if (block) items.push(block); + return items.length === 0 ? undefined : rustBoundsOf(name, items); +} + +/** What the binding a Rust receiver names writes of its type, if it writes it. */ +function rustBindingType(binding: RustBinding, scopes: RustScopes, ref: UnresolvedRef, context: ResolutionContext, depth: number): RustWrittenType | null { + if (binding.type !== undefined) return { text: binding.type, file: ref.filePath, at: binding.at }; + if (binding.init !== undefined) return rustValueType(binding.init, binding.at, scopes, ref, context, depth); + if (binding.site) return rustSiteType(binding.site, binding.scrutinee, binding.at, scopes, ref, context, depth); + return null; +} + +/** The type of another local, as its binding in scope at `at` writes it. */ +function rustLocalType(name: string, at: number, scopes: RustScopes, ref: UnresolvedRef, context: ResolutionContext, depth: number): RustWrittenType | null { + if (depth > 3) return null; + const found = rustBindingAt(scopes, name, at); + return found && !found.ambiguous ? rustBindingType(found.binding, scopes, ref, context, depth + 1) : null; +} + +/** + * The type a Rust value is, where its text says: a struct literal or a + * tuple-struct's or variant's construction (`Opts { … }`, `Message::Text(s)`), + * a unit struct, a function's or an associated function's return + * (`Server::new()`, `load(…)?`), a `Box` / `Rc` / `Arc` of one, another + * local, a literal. Null when the text says nothing of it (a method chain, a + * block, a macro). + */ +function rustValueType(value: string, at: number, scopes: RustScopes, ref: UnresolvedRef, context: ResolutionContext, depth: number): RustWrittenType | null { + if (depth > 3) return null; + let v = value.trim(); + let unwraps = 0; + for (;;) { + const before = v; + v = v.replace(/\s*\.\s*await$/, ''); + if (v.endsWith('?')) { + v = v.slice(0, -1).trimEnd(); + unwraps++; + } + const tail = rustTrailingCall(v); + if (tail && /\.\s*(?:unwrap|expect)$/.test(tail.callee)) { + v = tail.callee.replace(/\s*\.\s*(?:unwrap|expect)$/, ''); + unwraps++; + } + if (v === before) break; + } + v = v.replace(/^&\s*(?:mut\s+)?/, '').trim(); + const local = (text: string): RustWrittenType => ({ text, file: ref.filePath, at }); + const unwrapped = (t: RustWrittenType | null): RustWrittenType | null => (t && unwraps > 0 ? rustUnwrapped(t, unwraps) : t); + // A literal, a closure or an array: nothing the project declares. + if (/^(?:[bc]?r#*)?"[^"]*"#*$|^b?'[^']*'$|^-?\d[\w.]*$|^(?:true|false)$|^(?:move\s+)?\|/.test(v) || + (v.startsWith('[') && rustGroupIsWhole(v))) return local('()'); + if (/^(?:vec|format)\s*!\s*[[({]/.test(v) && rustGroupIsWhole(v.slice(v.search(/[[({]/)))) return local(v.startsWith('vec') ? 'Vec<_>' : 'String'); + if (v.startsWith('(')) { + if (!rustGroupIsWhole(v)) return null; + const inner = v.slice(1, -1); + return splitRustTopLevel(inner, ',').length === 1 && !/,\s*$/.test(inner) + ? unwrapped(rustValueType(inner, at, scopes, ref, context, depth + 1)) : local('()'); + } + const call = rustTrailingCall(v); + if (call && /^[A-Za-z_][\w:\s<>,]*$/.test(call.callee) && !call.callee.includes('.')) { + const callee = call.callee.replace(/\s+/g, '').replace(/::<[^()]*>$/, ''); + if (!/^[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*$/.test(callee)) return null; + const segments = callee.split('::'); + const last = segments[segments.length - 1]!; + // `Box::new(x)`, `Arc::new(x)`: what it holds, reached through it. + const wrapper = /^(?:(?:std|alloc)::(?:boxed|rc|sync)::)?(Box|Rc|Arc)::(?:new|pin|from)$/.exec(callee)?.[1]; + if (wrapper && splitRustTopLevel(call.args, ',').length === 1) { + const held = rustValueType(call.args, at, scopes, ref, context, depth + 1); + return held && unwraps === 0 ? { ...held, wrappers: [wrapper, ...(held.wrappers ?? [])] } : null; + } + if (/^[A-Z]/.test(last)) { + // A tuple struct (`Wrapper(x)`) or a variant (`Message::Text(s)`, `Some(x)`). + if (segments.length === 1 && last === 'Some') return unwrapped(local('Option<_>')); + if (segments.length === 1 && (last === 'Ok' || last === 'Err')) return unwrapped(local('Result<_, _>')); + if (/^[A-Z][A-Z0-9_]*$/.test(last) && last.length > 1) return null; + if (segments.length >= 2 && /^[A-Z]/.test(segments[segments.length - 2]!)) return unwrapped(local(segments.slice(0, -1).join('::'))); + return unwrapped(local(callee)); + } + if (segments.length >= 2 && /^[A-Z]/.test(segments[segments.length - 2]!)) { + return rustAssociatedReturn(segments.slice(0, -1), last, unwraps, at, ref, context); + } + return rustFunctionReturn(segments.slice(0, -1), last, unwraps, at, ref, context); + } + // `x.clone()` is a value of `x`'s type. + if (call && call.args.trim() === '') { + const cloned = /^([a-z_]\w*)\s*\.\s*clone$/.exec(call.callee); + if (cloned) return unwrapped(rustLocalType(cloned[1]!, at, scopes, ref, context, depth)); + return null; + } + if (call) return null; + const literal = /^((?:[A-Za-z_]\w*\s*::\s*)*[A-Z]\w*)\s*(?:::\s*<[^{}]*>\s*)?\{/.exec(v); + if (literal) { + if (!rustGroupIsWhole(v.slice(v.indexOf('{')))) return null; + // A struct variant's literal is a value of its enum: `Outcome::Ignore { note: None }` is an `Outcome`. + const segments = literal[1]!.replace(/\s+/g, '').split('::'); + return unwrapped(local((segments.length >= 2 && /^[A-Z]/.test(segments[segments.length - 2]!) ? segments.slice(0, -1) : segments).join('::'))); + } + if (/^[A-Za-z_]\w*(?:\s*::\s*[A-Za-z_]\w*)*$/.test(v)) { + const segments = v.split('::').map((s) => s.trim()); + const last = segments[segments.length - 1]!; + if (segments.length === 1 && /^[a-z_]/.test(last)) return unwrapped(rustLocalType(last, at, scopes, ref, context, depth)); + if (!/^[A-Z]/.test(last) || (/^[A-Z][A-Z0-9_]*$/.test(last) && last.length > 1)) return null; + if (segments.length === 1 && last === 'None') return local('Option<_>'); + // A unit variant is a value of its enum: `Mode::Fast` is a `Mode`. + if (segments.length >= 2 && /^[A-Z]/.test(segments[segments.length - 2]!)) return local(segments.slice(0, -1).join('::')); + return unwrapped(local(segments.join('::'))); + } + return null; +} + +/** Where a node starts in its file, as an offset into rustScopesOf's text. */ +function rustNodeOffset(n: Node, context: ResolutionContext): number { + const scopes = rustScopesOf(n.filePath, context); + return (scopes?.lineStarts[n.startLine - 1] ?? 0) + (n.startColumn ?? 0); +} + +/** + * Where a function's return type is read from: its parameter list, inside + * the function, so its own generic parameters are in scope there — + * dioxus's `use_hook(…) -> State` returns whatever `State` is. + */ +function rustSignatureOffset(fn: Node, context: ResolutionContext): number { + const start = rustNodeOffset(fn, context); + const scopes = rustScopesOf(fn.filePath, context); + const own = scopes?.fns.find((f) => f.at >= start && f.at <= start + 512); + return own ? own.from + 1 : start; +} + +/** + * What `Type::f(…)` returns, read from the signature of the type's `f`: + * `Self` is the type. A type from outside the project makes, as far as a + * call on it goes, no project value; one whose `f` the index doesn't hold + * (a derived `Default`) is taken to make itself only through `new` and `default`. + */ +function rustAssociatedReturn(owner: string[], fn: string, unwraps: number, at: number, ref: UnresolvedRef, context: ResolutionContext): RustWrittenType | null { + const written: RustWrittenType = { text: owner.join('::'), file: ref.filePath, at }; + const read = rustReadType(written, ref, context); + // An outside type's function the index can't read: one that constructs + // makes the type (`File::open(p)?`, `Vec::with_capacity(n)`); another may + // hand out anything — `Arc::make_mut(&mut self.node)` is a `&mut Node`. + const constructs = /^(?:new|default|from|open|create|connect|bind|empty|(?:new|with|from)_\w+)$/.test(fn); + if (read.kind === 'outside') return constructs ? written : null; + if (read.kind !== 'named') return null; + const lookup = rustTypeLookup(read.name, read.path, read.written.file, context, read.written.at); + if (lookup.outside) return constructs ? written : null; + const fns = rustOwnedMethods(lookup.name, fn, lookup, context); + if (fns.length === 0) return (fn === 'new' || fn === 'default') && unwraps === 0 ? written : null; + const returns = new Set(fns.map((f) => rustReturnText(f.signature) ?? '')); + if (returns.size !== 1) return null; + const text = [...returns][0]!; + if (text === '') return null; + const made: RustWrittenType = { text, file: fns[0]!.filePath, at: rustSignatureOffset(fns[0]!, context) }; + const value = unwraps > 0 ? rustUnwrapped(made, unwraps) : made; + // `Self`, or the type's own name, is the type the call names, where it + // names it: `Semaphore::new(1)` under `use tokio::sync::Semaphore` is that + // `Semaphore`, whichever of the types the name may mean has the `new` read + // here, and so is ripgrep's `grep::regex::RegexMatcherBuilder::new()`, + // pcre2's `new` writing the same `-> RegexMatcherBuilder`. Any other text + // means what it does where it is written, which several types' files may + // read differently. + if (value && (value.text.trim() === 'Self' || rustPathHead(value.text)?.name === read.name)) return written; + return new Set(fns.map((f) => f.filePath)).size > 1 ? null : value; +} + +/** What a free function `f(…)` or `module::f(…)` returns, read from its signature, when one function of the name is in reach. */ +function rustFunctionReturn(modules: string[], fn: string, unwraps: number, at: number, ref: UnresolvedRef, context: ResolutionContext): RustWrittenType | null { + let fns = context.getNodesByName(fn).filter((n) => n.language === 'rust' && n.kind === 'function' && n.name === fn); + const same = fns.filter((n) => n.filePath === ref.filePath); + if (same.length > 0 && modules.length === 0) { + fns = same; + } else { + const used = modules.length > 0 ? undefined : rustUsePath(fn, ref.filePath, at, context); + if (modules.length === 0 && !used) return null; + const walked = (used ? used.slice(0, -1) : modules).filter((s) => s !== 'crate' && s !== 'self' && s !== 'super' && !rustProjectCrates(context).has(s)); + if (walked.length > 0) fns = fns.filter((n) => rustModulesLead(walked, n)); + } + const returns = new Set(fns.map((f) => rustReturnText(f.signature) ?? '')); + if (fns.length === 0 || returns.size !== 1) return null; + const text = [...returns][0]!; + if (text === '') return null; + const made: RustWrittenType = { text, file: fns[0]!.filePath, at: rustSignatureOffset(fns[0]!, context) }; + return unwraps > 0 ? rustUnwrapped(made, unwraps) : made; +} + +/** + * The type a name a pattern binds is: in `Some(x)` / `Ok(x)` / `Err(e)` what + * the matched value's `Option` or `Result` holds; in a variant's or a + * struct's pattern (`Commands::Build(opts)`, `Point { x, .. }`) the field's + * type its declaration writes. + */ +function rustSiteType(site: RustPatternSite, scrutinee: string | undefined, at: number, scopes: RustScopes, ref: UnresolvedRef, context: ResolutionContext, depth: number): RustWrittenType | null { + const segments = site.path.split('::'); + const last = segments[segments.length - 1]!; + if (segments.length === 1 && (last === 'Some' || last === 'Ok' || last === 'Err')) { + if (site.index !== 0 || !scrutinee) return null; + const value = rustValueType(scrutinee, at, scopes, ref, context, depth + 1); + const head = value && rustPathHead(value.text.replace(/^&\s*(?:'\w+\s+)?(?:mut\s+)?/, '')); + if (!value || !head) return null; + const args = splitRustTopLevel(head.args, ','); + if (last === 'Some' && head.name === 'Option' && args.length === 1) return { ...value, text: args[0]! }; + if (last === 'Ok' && head.name === 'Result' && args.length >= 1) return { ...value, text: args[0]! }; + if (last === 'Err' && head.name === 'Result' && args.length === 2) return { ...value, text: args[1]! }; + return null; + } + const decl = rustPatternDeclaration(segments, at, ref, context); + if (!decl) return null; + const text = site.index !== undefined ? decl.fields.tuple?.[site.index] : site.field !== undefined ? decl.fields.named?.get(site.field) : undefined; + return text ? { text, file: decl.file, at: decl.at, self: decl.self, generics: decl.generics } : null; +} + +/** The declaration a tuple-struct, variant or struct pattern's path names, with its fields' types. */ +function rustPatternDeclaration(segments: string[], at: number, ref: UnresolvedRef, context: ResolutionContext): { fields: RustFields; file: string; at: number; self: string; generics: string } | null { + const last = segments[segments.length - 1]!; + if (segments.length >= 2) { + // A variant: the enum is the path before it. + const owner = rustReadType({ text: segments.slice(0, -1).join('::'), file: ref.filePath, at }, ref, context); + if (owner.kind !== 'named') return null; + const enums = rustTypeLookup(owner.name, owner.path, owner.written.file, context, owner.written.at).nodes.filter((n) => n.kind === 'enum'); + if (enums.length !== 1) return null; + const enumNode = enums[0]!; + const variant = (context.getNodesInFileNamed?.(enumNode.filePath, last) ?? context.getNodesInFile(enumNode.filePath)) + .find((n) => n.kind === 'enum_member' && n.qualifiedName === `${enumNode.qualifiedName}::${last}`); + if (!variant) return null; + const fields = rustDeclaredFields(variant, context); + const generics = rustDeclaredFields(enumNode, context, true)?.generics ?? ''; + return fields ? { fields, file: variant.filePath, at: rustNodeOffset(variant, context), self: enumNode.name, generics } : null; + } + const structs = rustTypeLookup(last, [], ref.filePath, context, at).nodes.filter((n) => n.kind === 'struct'); + if (structs.length !== 1) return null; + const fields = rustDeclaredFields(structs[0]!, context); + return fields ? { fields, file: structs[0]!.filePath, at: rustNodeOffset(structs[0]!, context), self: structs[0]!.name, generics: fields.generics } : null; +} + +/** A struct's or a variant's fields: a tuple's types in order, or a named field's type by its name. */ +interface RustFields { + tuple?: string[]; + named?: Map; + /** The declaration's own generic parameters. */ + generics: string; +} + +/** The fields a struct, an enum's variant (or, for its generics, an enum) declares, read from its file. */ +const RUST_DECLARED_FIELDS = new WeakMap>(); + +function rustDeclaredFields(decl: Node, context: ResolutionContext, genericsOnly = false): RustFields | null { + let memo = RUST_DECLARED_FIELDS.get(context); + if (!memo) RUST_DECLARED_FIELDS.set(context, (memo = new Map())); + const key = `${decl.id}${genericsOnly ? '\u0000g' : ''}`; + if (memo.has(key)) return memo.get(key)!; + const fields = readRustDeclaredFields(decl, context, genericsOnly); + memo.set(key, fields); + return fields; +} + +function readRustDeclaredFields(decl: Node, context: ResolutionContext, genericsOnly: boolean): RustFields | null { + const scopes = rustScopesOf(decl.filePath, context); + if (!scopes) return null; + const code = scopes.code; + const start = rustNodeOffset(decl, context); + const name = new RegExp(`\\b${decl.name}\\b`).exec(code.slice(start, start + 512)); + if (!name) return null; + let p = start + name.index + decl.name.length; + while (p < code.length && /\s/.test(code[p]!)) p++; + let generics = ''; + if (code[p] === '<') { + const end = rustAngleEnd(code.slice(p, p + 2000), 0); + if (end < 0) return null; + generics = code.slice(p + 1, p + end); + p += end + 1; + } + if (genericsOnly) return { generics }; + // Past a `where` clause, to the fields' bracket. + let open = p; + while (open < code.length && code[open] !== '(' && code[open] !== '{' && code[open] !== ';' && code[open] !== ',' && code[open] !== '}') open++; + const bracket = code[open]; + if (bracket !== '(' && bracket !== '{') return { generics }; + let depth = 0; + let close = open; + for (; close < code.length; close++) { + const c = code[close]; + if (c === '(' || c === '[' || c === '{') depth++; + else if ((c === ')' || c === ']' || c === '}') && --depth === 0) break; + } + const field = (text: string): string => text.replace(/#\s*\[[^[\]]*(?:\[[^[\]]*\][^[\]]*)*\]/g, ' ').replace(/^\s*pub\s*(?:\([^)]*\))?/, '').trim(); + const parts = splitRustTopLevel(code.slice(open + 1, close), ',').map(field); + if (bracket === '(') return { tuple: parts, generics }; + const named = new Map(); + for (const part of parts) { + const m = /^([A-Za-z_]\w*)\s*:\s*([\s\S]+)$/.exec(part); + if (m) named.set(m[1]!, m[2]!.trim()); + } + return { named, generics }; +} + +/** `Name::method` nodes the index holds, of any type of that name. */ +function rustMethodsNamed(name: string, method: string, context: ResolutionContext): Node[] { + if (context.getMethodMatches) return context.getMethodMatches(name, method, 'rust').filter((m) => m.language === 'rust'); + const want = `${name}::${method}`; + return context.getNodesByName(method).filter((m) => m.kind === 'method' && m.language === 'rust' && + (m.qualifiedName === want || m.qualifiedName.endsWith(`::${want}`))); +} + +/** + * The `Name::method` nodes of the types a lookup found: a namesake type's + * methods are its own. A method is the type's when it sits in the type's + * file, or its impl's file names the type by that name. + */ +function rustOwnedMethods(name: string, method: string, lookup: RustTypeLookup, context: ResolutionContext): Node[] { + // A trait's own declaration of the method is the trait's, whatever type + // shares its name: gleam's `struct HttpClient` implements `trait HttpClient`. + const matches = rustMethodsNamed(name, method, context).filter((m) => rustTraitOf(m, context) === null); + const types = lookup.nodes.filter((n) => RUST_OWNER_KINDS.has(n.kind)); + if (types.length === 0) return matches; + const namesakes = context.getNodesByName(name).filter((n) => n.language === 'rust' && n.name === name && RUST_OWNER_KINDS.has(n.kind)); + if (namesakes.length <= types.length) return matches; + const ids = new Set(types.map((t) => t.id)); + const files = new Set(types.map((t) => t.filePath)); + return matches.filter((m) => files.has(m.filePath) || rustTypeLookup(name, [], m.filePath, context, rustNodeOffset(m, context)).nodes.some((n) => ids.has(n.id))); +} + +/** The text an `impl` header above a Rust method gives its implementing type's arguments: `BuildArgs` of `impl Overrides`. */ +function rustImplSelfArgs(method: Node, context: ResolutionContext): { args: string; generics: string; byRef: boolean } | null { + const scopes = rustScopesOf(method.filePath, context); + if (!scopes) return null; + const block = rustImplAt(scopes, rustNodeOffset(method, context)); + if (!block || block.kind !== 'impl') return null; + const header = scopes.code.slice(block.at, block.from).replace(/\bwhere\b[\s\S]*$/, ''); + const head = header.replace(/^impl\s*(?:<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/, ''); + const self = /\bfor\s+([\s\S]+)$/.exec(head)?.[1] ?? head; + const type = rustPathHead(self.trim().replace(/^&\s*(?:'\w+\s+)?(?:mut\s+)?/, '')); + return type ? { args: type.args, generics: block.generics, byRef: self.trim().startsWith('&') } : null; +} + +/** + * Among methods of one name in several impls of a type, those whose impl is + * for the type's arguments the receiver has: `impl Overrides`'s + * for an `Overrides`, before an `impl Overrides`'s. + */ +function rustSpecialized(methods: Node[], args: string, context: ResolutionContext): Node[] { + if (methods.length < 2 || args === '') return methods; + const norm = (text: string): string[] => splitRustTopLevel(text, ',').map((a) => a.replace(/\s+/g, '').replace(/^(?:[A-Za-z_]\w*::)+/, '')); + const want = norm(args); + const exact: Node[] = []; + const generic: Node[] = []; + for (const m of methods) { + const impl = rustImplSelfArgs(m, context); + if (!impl) { generic.push(m); continue; } + const have = norm(impl.args); + if (have.length !== want.length) continue; + if (have.every((a, i) => a === want[i])) exact.push(m); + else if (have.every((a, i) => a === want[i] || rustBoundsOf(a, [{ generics: impl.generics, where: '' }]) !== undefined)) generic.push(m); + } + return exact.length > 0 ? exact : generic.length > 0 ? generic : methods; +} + +/** The method among `methods` a call reaches: its own file's, the type's file's, then the nearest. */ +function rustPickMethod(methods: Node[], typeFiles: Set, ref: UnresolvedRef, confidence: number): ResolvedRef { + const chosen = methods.find((m) => m.filePath === ref.filePath) ?? methods.find((m) => typeFiles.has(m.filePath)) ?? + methods.reduce((best, m) => (computePathProximity(ref.filePath, m.filePath) > computePathProximity(ref.filePath, best.filePath) ? m : best)); + return { original: ref, targetNodeId: chosen.id, confidence, resolvedBy: 'instance-method' }; +} + +/** Whether a trait whose impl a Rust call may reach is callable from the call's file: a project trait in scope there; any other. */ +function isRustTraitCallable(trait: string, ref: UnresolvedRef, sample: Node, context: ResolutionContext): boolean { + return !isRustProjectNamed(trait, RUST_TRAIT_KIND, context) || isRustTraitInScope(trait, ref, sample, context); +} + +const RUST_TRAIT_OF = new WeakMap>(); + +/** The trait whose body declares a Rust method, if one does. */ +function rustTraitOf(method: Node, context: ResolutionContext): Node | null { + let memo = RUST_TRAIT_OF.get(context); + if (!memo) RUST_TRAIT_OF.set(context, (memo = new Map())); + const hit = memo.get(method.id); + if (hit !== undefined) return hit; + const cut = method.qualifiedName.lastIndexOf('::'); + const owner = cut > 0 ? method.qualifiedName.slice(0, cut).split('::').pop()! : ''; + const trait = owner === '' ? null : (context.getNodesInFileNamed?.(method.filePath, owner) ?? context.getNodesInFile(method.filePath)) + .find((n) => n.kind === 'trait' && n.name === owner && n.startLine <= method.startLine && (n.endLine ?? 0) >= method.startLine) ?? null; + memo.set(method.id, trait); + return trait; +} + +const RUST_TRAIT_IMPLS = new WeakMap>(); + +/** + * Whether the trait named `trait` (its declaration `traitFile`, when the + * project's) is implemented for the type `name`: by an `impl` the type's + * files or the trait's file write (`impl Render for Widget {}`), or by a + * blanket impl over a generic (`impl AsyncReadExt for + * R {}`) whose bounds the value meets — each one of the traits it is known by + * (`receiverTraits`), or implemented for the type in turn. A blanket impl + * with bounds a type from outside the project can't be checked against + * gives it nothing. + */ +function rustTraitImplemented(trait: string, traitFile: string | null, name: string | null, typeFiles: ReadonlySet, + receiverTraits: readonly string[], context: ResolutionContext, depth = 0): boolean { + if (!/^[A-Za-z_]\w*$/.test(trait) || (name !== null && !/^[A-Za-z_]\w*$/.test(name))) return false; + let memo = RUST_TRAIT_IMPLS.get(context); + if (!memo) RUST_TRAIT_IMPLS.set(context, (memo = new Map())); + const files = [...new Set([...(traitFile ? [traitFile] : []), ...typeFiles])]; + const key = [trait, traitFile ?? '', name ?? '', receiverTraits.join(','), depth, ...files].join('\u0000'); + const hit = memo.get(key); + if (hit !== undefined) return hit; + memo.set(key, false); + const traitPart = String.raw`\bimpl\b\s*(?:<((?:[^<>{;]|<(?:[^<>{;]|<[^<>{;]*>)*>)*)>)?\s*(?:[\w:]+::)?${trait}\b(?:\s*<[^{;]*?>)?\s+for\s+(?:&\s*(?:'\w+\s+)?(?:mut\s+)?)?(?:[\w:]+::)?`; + const blanket = new RegExp(`${traitPart}([A-Z]\\w*)\\s*(?:where\\b([^{]*))?\\{`, 'g'); + const direct = name === null ? null : new RegExp(`${traitPart}${name}\\b`); + const meets = (bound: string): boolean => { + const head = rustPathHead(bound)?.name ?? ''; + if (RUST_MARKER_TRAITS.has(head) || receiverTraits.some((t) => rustPathHead(t)?.name === head)) return true; + if (name === null || depth >= 2) return false; + const declared = context.getNodesByName(head).find((n) => n.kind === 'trait' && n.language === 'rust'); + return rustTraitImplemented(head, declared?.filePath ?? null, name, typeFiles, receiverTraits, context, depth + 1); + }; + let found = false; + for (const file of files) { + const code = rustScopesOf(file, context)?.code ?? ''; + if (!code.includes(trait)) continue; + if (direct?.test(code)) { + found = true; + break; + } + for (const m of code.matchAll(blanket)) { + const bounds = m[1] === undefined ? undefined : rustBoundsOf(m[2]!, [{ generics: m[1], where: m[3] ?? '' }]); + if (bounds !== undefined && bounds.every(meets)) { + found = true; + break; + } + } + if (found) break; + } + memo.set(key, found); + return found; +} + +/** + * The method a call on a value of the type `name` — or known by + * `receiverTraits` — reaches as one a project trait provides: declared with a + * body in the trait, which the impl for the type (or a blanket one) doesn't + * override, the trait in scope at the call. Undefined when no such trait + * declares the method. + */ +function rustProvidedMethod(name: string | null, typeFiles: ReadonlySet, receiverTraits: readonly string[], method: string, + ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { + const declared = context.getNodesByName(method).filter((m) => m.kind === 'method' && m.language === 'rust' && m.name === method && + rustTraitOf(m, context) !== null && rustHasBody(m, context)); + if (declared.length === 0) return undefined; + const reached = declared.filter((m) => { + const trait = rustTraitOf(m, context)!; + return rustTraitImplemented(trait.name, trait.filePath, name, typeFiles, receiverTraits, context) && isRustTraitInScope(trait.name, ref, m, context); + }); + const traits = new Set(reached.map((m) => rustTraitOf(m, context)!.id)); + if (traits.size === 1) return { original: ref, targetNodeId: reached[0]!.id, confidence: 0.85, resolvedBy: 'instance-method' }; + return traits.size > 1 ? null : undefined; +} + +const RUST_ALIAS_IMPL_METHODS = new WeakMap>(); + +/** The inherent methods named `method` that impls of aliases of the type `name` declare: `TypedConstant::type_` for `Constant`. */ +function rustAliasImplMethods(name: string, method: string, context: ResolutionContext): Node[] { + let memo = RUST_ALIAS_IMPL_METHODS.get(context); + if (!memo) RUST_ALIAS_IMPL_METHODS.set(context, (memo = new Map())); + const key = `${name}\u0000${method}`; + const hit = memo.get(key); + if (hit) return hit; + const aliasOf = new Map(); + const isAliasOfType = (owner: string): boolean => { + let yes = aliasOf.get(owner); + if (yes === undefined) { + yes = owner !== name && context.getNodesByName(owner).some((n) => n.kind === 'type_alias' && n.language === 'rust' && + rustPathHead(rustAliasTarget(n, context)?.text ?? '')?.name === name); + aliasOf.set(owner, yes); + } + return yes; + }; + const found = context.getNodesByName(method).filter((m) => { + if (m.kind !== 'method' || m.language !== 'rust' || m.name !== method || rustTraitOf(m, context) !== null || isRustTraitImplMethod(m, context)) return false; + const cut = m.qualifiedName.lastIndexOf('::'); + return cut > 0 && isAliasOfType(m.qualifiedName.slice(0, cut).split('::').pop()!); + }); + memo.set(key, found); + return found; +} + +/** Whether a trait's method declaration has a body — a provided method — rather than ending at its `;`. */ +function rustHasBody(method: Node, context: ResolutionContext): boolean { + const scopes = rustScopesOf(method.filePath, context); + if (!scopes) return false; + const own = scopes.fns.find((f) => f.at >= rustNodeOffset(method, context) && rustLineAt(scopes, f.at) <= (method.endLine ?? method.startLine)); + return own !== undefined; +} + +/** What `impl Deref for Name { type Target = … }` makes a value of the type `name` hand calls on to. */ +function rustDerefTarget(name: string, typeFiles: Iterable, context: ResolutionContext): RustWrittenType | null { + const header = new RegExp(String.raw`\bimpl\b\s*(?:<[^{;]*?>)?\s*(?:(?:std|core)\s*::\s*ops\s*::\s*|ops\s*::\s*)?Deref\s+for\s+(?:[\w:]+::)?${name}\b[^{;]*\{`, 'g'); + for (const file of typeFiles) { + const code = rustScopesOf(file, context)?.code ?? ''; + if (!code.includes('Deref')) continue; + for (const m of code.matchAll(header)) { + const body = m.index! + m[0].length; + const target = /\btype\s+Target\s*=\s*([^;]+);/.exec(code.slice(body, body + 600)); + if (target) return { text: target[1]!.trim(), file, at: body + target.index! }; + } + } + return null; +} + +/** What a `type Name<…> = Target;` alias names, as its declaration writes it. */ +function rustAliasTarget(alias: Node, context: ResolutionContext): RustWrittenType | null { + const scopes = rustScopesOf(alias.filePath, context); + if (!scopes) return null; + const start = rustNodeOffset(alias, context); + const m = new RegExp(String.raw`\btype\s+${alias.name}\b\s*(?:<([^=;]*?)>)?\s*=\s*([^;]+);`).exec(scopes.code.slice(Math.max(0, start - 64), start + 2000)); + return m ? { text: m[2]!.trim(), file: alias.filePath, at: start, generics: m[1] ?? '' } : null; +} + +/** The methods named `method` a Rust trait declares, or the traits it extends do. */ +function rustTraitMethods(traits: Node[], method: string, context: ResolutionContext, depth = 0): Node[] { + const found: Node[] = []; + for (const trait of traits) { + const own = context.getNodesByName(method).filter((m) => m.kind === 'method' && m.language === 'rust' && m.filePath === trait.filePath && + m.startLine >= trait.startLine && m.startLine <= (trait.endLine ?? m.startLine) && rustTraitOf(m, context)?.id === trait.id); + if (own.length > 0) { + found.push(own[0]!); + continue; + } + if (depth >= 4) continue; + // `trait Name<…>: Super + Other where … {` + const scopes = rustScopesOf(trait.filePath, context); + const start = rustNodeOffset(trait, context); + const header = scopes ? new RegExp(String.raw`\btrait\s+${trait.name}\b\s*(?:<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?\s*:([^{;]*?)(?:\bwhere\b[^{]*)?\{`).exec(scopes.code.slice(start, start + 2000)) : null; + if (!header) continue; + for (const bound of splitRustTopLevel(header[1]!, '+')) { + const head = rustPathHead(bound); + if (!head || bound.startsWith('\'')) continue; + const supers = rustTypeLookup(head.name, head.segments, trait.filePath, context, rustNodeOffset(trait, context)).nodes.filter((n) => n.kind === 'trait'); + found.push(...rustTraitMethods(supers, method, context, depth + 1)); + } + } + return found; +} + +/** + * A call on a value known by its traits — a trait object, an `impl Trait`, a + * bounded generic — is to the method one of the project's traits among them + * (or a trait they extend) declares, or one a blanket impl provides in scope. + * Known only by traits from outside the project (`Box`), + * it is any implementation's, the project's among them: 'unread'. + */ +function rustTraitsMethodCall(traits: string[], written: RustWrittenType, method: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | 'unread' { + const found: Node[] = []; + let projectTraits = 0; + for (const bound of traits) { + const head = rustPathHead(bound); + if (!head) continue; + const nodes = rustTypeLookup(head.name, head.segments, written.file, context, written.at).nodes.filter((n) => n.kind === 'trait'); + if (nodes.length > 0) projectTraits++; + found.push(...rustTraitMethods(nodes, method, context)); + } + if (projectTraits === 0) return 'unread'; + const owners = new Set(found.map((m) => rustTraitOf(m, context)?.id)); + if (owners.size === 1) return { original: ref, targetNodeId: found[0]!.id, confidence: 0.9, resolvedBy: 'instance-method' }; + if (owners.size > 1) return null; + return rustProvidedMethod(null, new Set(), traits, method, ref, context) ?? null; +} + +/** Whether a method's body calls the function of its own name on its type: `Self::accept(self)`, `TcpListener::accept(self)`. */ +function rustDelegatesToOwn(method: Node, typeName: string, context: ResolutionContext): boolean { + const scopes = rustScopesOf(method.filePath, context); + if (!scopes) return false; + const start = scopes.lineStarts[method.startLine - 1] ?? 0; + const end = scopes.lineStarts[method.endLine ?? method.startLine] ?? scopes.code.length; + return new RegExp(String.raw`\b(?:Self|${typeName})\s*::\s*${method.name}\s*\(`).test(scopes.code.slice(start, end)); +} + +/** + * A call on a value of a type from outside the project — std's, a + * dependency's — reaches a project method only through a project trait in + * scope: one implemented for that type (`impl Ext for String`), or one a + * blanket impl gives every type. + */ +function rustOutsideTypeCall(name: string, args: string, byRef: boolean, method: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { + // Among impls for the type's instantiations, the one for its arguments: + // axum's `Result<(), E>` is `impl IntoResponse for Result`'s, not + // `Result`'s. One that hands the call on to the type's own function of + // its name (`fn local_addr(&self) { Self::local_addr(self) }` in axum's + // `impl Listener for TcpListener`) shows the type has that method itself, + // which a call reaches first. + const impls = rustSpecialized(rustMethodsNamed(name, method, context).filter((m) => { + const impl = rustTraitImplHeader(m, context); + return impl !== null && impl.self === name && isRustProjectNamed(impl.trait, RUST_TRAIT_KIND, context) && + isRustTraitInScope(impl.trait, ref, m, context) && !rustDelegatesToOwn(m, name, context); + }), args, context); + if (impls.length === 1) return { original: ref, targetNodeId: impls[0]!.id, confidence: 0.85, resolvedBy: 'instance-method' }; + if (impls.length > 1) { + // A reference's own impl for one, a value's for the value: gleam's + // `impl Documentable for &EcoString` beside the one for `EcoString`. + const matching = impls.filter((m) => (rustImplSelfArgs(m, context)?.byRef ?? false) === byRef); + const pool = matching.length > 0 ? matching : impls; + if (pool.length === 1) return { original: ref, targetNodeId: pool[0]!.id, confidence: 0.85, resolvedBy: 'instance-method' }; + // Two traits' methods: Rust asks which. One trait's, in impls `#[cfg]` twins: the nearest. + const traits = new Set(pool.map((m) => { + const impl = rustTraitImplHeader(m, context); + return `${impl?.trait}<${impl?.traitArgs.join(',')}>`; + })); + return traits.size === 1 ? rustPickMethod(pool, new Set(), ref, 0.85) : null; + } + // An impl for a primitive names no type the index can address its methods + // by — gleam's `impl Documentable for usize` — so the call is the trait's + // method, where the trait is in scope. + if (RUST_PRIMITIVE_TYPES.has(name)) { + const declared = context.getNodesByName(method).filter((m) => m.kind === 'method' && m.language === 'rust' && m.name === method && rustTraitOf(m, context) !== null) + .filter((m) => { + const trait = rustTraitOf(m, context)!; + return rustTraitImplemented(trait.name, trait.filePath, name, new Set(), [], context) && isRustTraitInScope(trait.name, ref, m, context); + }); + const traits = new Set(declared.map((m) => rustTraitOf(m, context)!.id)); + if (traits.size === 1) return { original: ref, targetNodeId: declared[0]!.id, confidence: 0.8, resolvedBy: 'instance-method' }; + if (traits.size > 1) return null; + } + return rustProvidedMethod(/^[A-Za-z_]\w*$/.test(name) ? name : null, new Set(), [], method, ref, context) ?? null; +} + +/** + * The method a Rust call reaches on a value of a type as read: its type's + * own (inherent) method first, then one of a trait impl for the type — the + * trait in scope at the call when several have one — then one a trait it + * implements provides, then what its `Deref` hands the call on to. A + * method `Box`, `Rc` or `Arc` has itself (`clone`) is theirs. Null when the + * type has no such method the project declares; 'unread' when what the type + * is can't be told. + */ +function rustTypedMethodCall(read: RustTypeRead, method: string, ref: UnresolvedRef, context: ResolutionContext, depth = 0): ResolvedRef | null | 'unread' { + if (read.kind === 'unread') return 'unread'; + if (read.wrappers.some((w) => RUST_WRAPPER_OWN_METHODS.get(w)?.has(method))) return null; + // A wrapper the project declares itself — tokio's `MutexGuard` — has its + // own methods, which a call reaches before what it holds. + for (const wrapper of read.wrappers) { + const own = rustMethodsNamed(wrapper, method, context).filter((m) => !isRustTraitImplMethod(m, context) && rustTraitOf(m, context) === null); + if (own.length === 0) continue; + const declared = rustTypeLookup(wrapper, [], read.written.file, context, read.written.at); + if (!declared.outside && declared.nodes.length > 0) return rustPickMethod(own, new Set(declared.nodes.map((n) => n.filePath)), ref, 0.9); + } + // A project impl on the std wrapper itself — tokio's `impl

AsyncWrite + // for Pin

`, `impl Schedule for Arc` — answers before what the + // wrapper holds, as Rust tries the wrapper before dereferencing it + // (rustThroughWrapper). Only the held type's methods taking `self` through + // that wrapper (`fn poll_flush(self: Pin<&mut Self>, …)`) come first. + const outer = read.wrappers[0]; + if (outer !== undefined && RUST_SELF_WRAPPERS.has(outer) && !isRustProjectNamed(outer, RUST_DECLARED_TYPE_KINDS, context)) { + const held = read.kind === 'named' ? read.name : null; + const onWrapper = context.getNodesByName(method).filter((n) => n.language === 'rust' && n.kind === 'method' && + (n.qualifiedName === `${outer}::${method}` || n.qualifiedName.endsWith(`::${outer}::${method}`)) && + rustSelfWrapper(n) === null && rustImplFor(n, held, context)); + if (onWrapper.length > 0) { + const inner = rustTypedMethodCall({ ...read, wrappers: read.wrappers.slice(1) }, method, ref, context, depth); + const target = inner && inner !== 'unread' ? context.getNodeById?.(inner.targetNodeId) : undefined; + if (inner && inner !== 'unread' && target && rustSelfWrapper(target) === outer) return inner; + return { original: ref, targetNodeId: preferCallSiteFile(onWrapper, ref.filePath)[0]!.id, confidence: 0.9, resolvedBy: 'instance-method' }; + } + } + if (read.kind === 'outside') return rustOutsideTypeCall(read.name, '', read.byRef, method, ref, context); + if (read.kind === 'traits') return rustTraitsMethodCall(read.traits, read.written, method, ref, context); + const lookup = rustTypeLookup(read.name, read.path, read.written.file, context, read.written.at); + if (lookup.outside) return rustOutsideTypeCall(lookup.name, read.args, read.byRef, method, ref, context); + // No project type has the name — a dependency's, or one a macro declares: + // only methods the index holds for that name make it the project's. + if (lookup.undeclared && rustMethodsNamed(lookup.name, method, context).length === 0) return rustOutsideTypeCall(lookup.name, read.args, read.byRef, method, ref, context); + if (lookup.nodes.length > 0 && lookup.nodes.every((n) => n.kind === 'trait')) { + return rustTraitsMethodCall([read.name], read.written, method, ref, context); + } + const alias = lookup.nodes.length === 1 && lookup.nodes[0]!.kind === 'type_alias' ? lookup.nodes[0]! : null; + if (alias) { + // `impl TypedStatement { … }` on gleam's `type TypedStatement = + // Statement<…>` keeps its methods under the alias's name; the type it + // names has the rest. + const own = rustMethodsNamed(alias.name, method, context); + const inherent = own.filter((m) => !isRustTraitImplMethod(m, context) && rustTraitOf(m, context) === null); + if (inherent.length > 0) return rustPickMethod(inherent, new Set([alias.filePath]), ref, 0.9); + const target = depth < 4 ? rustAliasTarget(alias, context) : null; + return target ? rustTypedMethodCall(rustReadType({ ...target, wrappers: read.wrappers }, ref, context), method, ref, context, depth + 1) : 'unread'; + } + let types = lookup.nodes.filter((n) => RUST_OWNER_KINDS.has(n.kind)); + // Several types the name may mean: the one with the method — tokio's + // `sync::Semaphore` has `try_acquire_owned`, its `batch_semaphore` namesake + // doesn't. Otherwise, as the name strategies take it. + if (types.length > 1 && new Set(types.map((t) => t.filePath)).size > 1) { + const having = types.filter((t) => rustOwnedMethods(lookup.name, method, { ...lookup, nodes: [t] }, context).length > 0); + if (having.length !== 1) return 'unread'; + types = having; + } + const typeFiles = new Set(types.map((t) => t.filePath)); + const matches = rustOwnedMethods(lookup.name, method, { ...lookup, nodes: lookup.nodes.length > 0 ? types : [] }, context); + const inherent = matches.filter((m) => !isRustTraitImplMethod(m, context)); + if (inherent.length > 0) return rustPickMethod(rustSpecialized(inherent, read.args, context), typeFiles, ref, 0.9); + const viaTraits = new Map(); + for (const m of matches) { + const trait = rustTraitImplHeader(m, context)?.trait ?? ''; + viaTraits.set(trait, [...(viaTraits.get(trait) ?? []), m]); + } + if (viaTraits.size > 0) { + let traits = [...viaTraits.keys()]; + if (traits.length > 1) traits = traits.filter((t) => isRustTraitCallable(t, ref, viaTraits.get(t)![0]!, context)); + // Two traits in scope with the method: Rust asks which. + if (traits.length !== 1) return null; + const methods = rustSpecialized(viaTraits.get(traits[0]!)!, read.args, context); + // `impl From for T` and `impl From for T`: the argument decides. + if (new Set(methods.map((m) => rustTraitImplHeader(m, context)?.traitArgs.join(','))).size > 1) return null; + return rustPickMethod(methods, typeFiles, ref, 0.9); + } + // An impl of an alias of the type, for the instantiation it names: gleam's + // `impl TypedConstant { fn type_() }` on `type TypedConstant = Constant<…>`, + // called on a `Constant` a variant of a generic enum holds. + const viaAlias = rustAliasImplMethods(lookup.name, method, context); + if (viaAlias.length > 0) { + const owners = new Set(viaAlias.map((m) => m.qualifiedName.slice(0, m.qualifiedName.lastIndexOf('::')))); + return owners.size === 1 ? rustPickMethod(viaAlias, typeFiles, ref, 0.85) : null; + } + const provided = rustProvidedMethod(lookup.name, typeFiles, [], method, ref, context); + if (provided !== undefined) return provided; + if (depth < 3) { + const target = rustDerefTarget(lookup.name, typeFiles, context); + if (target) return rustTypedMethodCall(rustReadType(target, ref, context), method, ref, context, depth + 1); + } + return null; +} + +/** + * A Rust method call on a local or a parameter — `opts.build()` — as the + * type its binding in scope declares makes it (rustTypedMethodCall): the + * parameter's type, the `let`'s annotation or the value it is built from, + * the field a match arm's variant holds there. Undefined when no binding of + * the name is in scope where the call is (a `static`, a name a macro binds); + * `{ unread }`, with the binding's line, when the binding doesn't say what + * its type is. + */ +function matchRustTypedReceiverCall(receiver: string, method: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | { unread: number } | undefined { + const result = rustTypedReceiverCall(receiver, method, ref, context); + if (result && 'targetNodeId' in result) { + let typed = RUST_TYPED_REFS.get(context); + if (!typed) RUST_TYPED_REFS.set(context, (typed = new WeakSet())); + typed.add(ref); + } + return result; +} + +/** The refs matchRustTypedReceiverCall resolved: through a typed receiver, a trait's own method is in reach. */ +const RUST_TYPED_REFS = new WeakMap>(); + +function rustTypedReceiverCall(receiver: string, method: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | { unread: number } | undefined { + const scopes = rustScopesOf(ref.filePath, context); + if (!scopes) return undefined; + const offset = (scopes.lineStarts[ref.line - 1] ?? 0) + Math.max(0, ref.column); + const found = rustBindingAt(scopes, receiver, offset); + if (!found) return undefined; + const unread = { unread: rustLineAt(scopes, found.binding.at) }; + if (found.ambiguous) return unread; + const written = rustBindingType(found.binding, scopes, ref, context, 0); + if (!written) return unread; + const result = rustTypedMethodCall(rustReadType(written, ref, context), method, ref, context); + return result === 'unread' ? unread : result; +} + /** What only exists inside a type, reachable through a receiver alone. */ const TYPE_MEMBER_KINDS: ReadonlySet = new Set(['method', 'property', 'field', 'enum_member']); @@ -15314,6 +16547,14 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { RUST_DEPENDENCIES.delete(context); RUST_CRATE_GRAPHS.delete(context); RUST_IMPL_HEADERS.delete(context); + RUST_SCOPES.delete(context); + RUST_USE_PATHS.delete(context); + RUST_WALKED_ITEMS.delete(context); + RUST_TRAIT_OF.delete(context); + RUST_TRAIT_IMPLS.delete(context); + RUST_TYPED_REFS.delete(context); + RUST_DECLARED_FIELDS.delete(context); + RUST_ALIAS_IMPL_METHODS.delete(context); LEXICAL_SCOPE_MEMO.delete(context); KOTLIN_LAMBDA_RECEIVERS.delete(context); SCALA_IMPORTED_SUPERS.delete(context); @@ -16167,7 +17408,10 @@ export function inferLocalReceiverType( const type = matchLine(i); if (type) { states.set(key, { hi: callIdx, ansIdx: i, ansType: type, ansRaw: lastRaw }); - if (decl) decl.raw = lastRaw; + if (decl) { + decl.raw = lastRaw; + decl.line = i + 1; + } return type; } } @@ -16186,13 +17430,19 @@ export function inferLocalReceiverType( } state.hi = callIdx; if (state.ansIdx < startIdx) return null; - if (decl) decl.raw = state.ansRaw; + if (decl) { + decl.raw = state.ansRaw; + decl.line = state.ansIdx + 1; + } return state.ansType; } for (let i = callIdx; i >= startIdx; i--) { const type = matchLine(i); if (type) { - if (decl) decl.raw = lastRaw; + if (decl) { + decl.raw = lastRaw; + decl.line = i + 1; + } return type; } } @@ -16599,6 +17849,20 @@ export function matchMethodCall( return matchRustSelfCall(methodName!, ref, context); } + // A Rust call on a local or a parameter is a method of the type its + // binding where the call is declares (matchRustTypedReceiverCall), or of + // no project type. dioxus's `Commands::Build(opts) => opts.build()` went + // unlinked once another type had a `build`. A binding that doesn't say its + // type leaves the call to the strategies below, which take no project + // type from another declaration of the name: the unscoped scan would find + // one the binding hides (`fn f(srv: Server) { if let Some(srv) = … }`). + let rustBindingLine: number | undefined; + if (ref.language === 'rust' && dotMatch && /^[a-z_]\w*$/.test(objectOrClass!) && objectOrClass !== 'self') { + const typed = nmTimedT('mc-rusttyped', ref, () => matchRustTypedReceiverCall(objectOrClass!, methodName!, ref, context)); + if (typed !== undefined && (typed === null || !('unread' in typed))) return typed; + if (typed !== undefined) rustBindingLine = typed.unread; + } + // Infer the receiver's type from its local declaration/initializer in the // enclosing scope, then resolve the method on that type (#1108). C++ keeps its // dedicated inferrer (header scan + `auto`); every other language uses the @@ -16619,6 +17883,13 @@ export function matchMethodCall( ref.language === 'cpp' ? inferCppReceiverType(objectOrClass!, ref, context, 0, cppDecl) : inferLocalReceiverType(objectOrClass!, ref, context, decl)); + // Read on another line than the binding the call sees, it is another + // binding's type: the strategies below guess, unless no project type has + // that name — as before the binding was read. + if (rustBindingLine !== undefined && inferredType && decl.line !== rustBindingLine) { + if (isUndeclaredTypeName(inferredType.split('.').pop()!, ref, context)) return null; + inferredType = null; + } // A C++ receiver the calling function or its class declares calls a // member of the class C++ name lookup finds for its type, not of any // class sharing the type's last name. @@ -17618,6 +18889,8 @@ interface GoReceiverDecl { raw?: string; asserted?: GoProjectType | null; declared?: string | null; + /** The line the inference read the declaration on. */ + line?: number; } const GO_ASSERTION_BINDINGS = new WeakMap>>(); diff --git a/src/resolution/rust-scopes.ts b/src/resolution/rust-scopes.ts new file mode 100644 index 00000000..4b896806 --- /dev/null +++ b/src/resolution/rust-scopes.ts @@ -0,0 +1,779 @@ +/** + * A Rust file's local bindings, read from its text, for typing the receiver + * of a method call (matchRustTypedReceiverCall in name-matcher.ts): every + * parameter of a function or a closure, `let`, `if let` / `while let`, + * `let … else`, `for` and match-arm binding, where it is visible, and what + * the source writes of its type there. + * + * A reader, not a parser. Comments and the contents of strings and + * characters are blanked first (offsets and newlines kept), brackets are + * paired, and each form is read from its keyword. A name a pattern may bind + * is bound, typed or not, so that it hides the declarations around it rather + * than taking their type: `fn f(srv: Server) { if let Some(srv) = spare() { + * srv.run() } }` calls `run` on what `spare()` holds, not on a `Server`. + */ + +/** Where a name sits in a tuple-struct, variant or struct pattern: `Commands::Build(opts)`, `Point { x, .. }`. */ +export interface RustPatternSite { + /** The pattern's path as written: `Commands::Build`, `Self::Text`, `Some`. */ + path: string; + /** The name's position among a tuple pattern's fields. */ + index?: number; + /** The field a struct pattern binds the name from. */ + field?: string; +} + +export interface RustBinding { + /** Offset of the bound name. */ + at: number; + kind: 'param' | 'closure' | 'let' | 'cond' | 'for' | 'arm'; + /** Where the name is visible: from `from` up to, not including, `to`. */ + from: number; + to: number; + /** The type written for it — a parameter's or a `let`'s — when its pattern is the name alone. */ + type?: string; + /** A `let`'s value, when its pattern is the name alone and it writes no type. */ + init?: string; + /** The pattern around the name, when it is bound inside a tuple-struct, variant or struct pattern. */ + site?: RustPatternSite; + /** What an `if let`, `while let`, `let … else` or `match` pattern is matched against. */ + scrutinee?: string; +} + +/** A function with a body: its generic parameters and `where` clause, and the span its parameters are bound in. */ +export interface RustFn { + /** Offset of the `fn` keyword. */ + at: number; + /** The text between its generic parameters' brackets ('' for none). */ + generics: string; + /** Its `where` clause's text after `where` ('' for none). */ + where: string; + /** From its parameter list's `(` to its body's `}`. */ + from: number; + to: number; + /** Its parameter list's `)`, and its body's `{`. */ + paramsEnd: number; + body: number; +} + +/** An `impl` or `trait` block: its generic parameters and `where` clause, and its body. */ +export interface RustImplBlock { + /** Offset of the keyword. */ + at: number; + kind: 'impl' | 'trait'; + generics: string; + where: string; + /** Its body, from `{` to `}`. */ + from: number; + to: number; +} + +export interface RustScopes { + /** The source with its comments and the contents of its strings and characters blanked. */ + code: string; + lineStarts: number[]; + fns: RustFn[]; + impls: RustImplBlock[]; + /** Each `(`, `[` and `{` → the bracket closing it. */ + close: Map; + /** Offsets of every `{` and `}`, with the innermost block open after each (rustInnerBlockAt). */ + events: number[]; + openAfter: number[]; + /** The names bound in the file, read on the first lookup (rustBindingAt). */ + bindings: Map | null; +} + +const IDENT_CHAR = /[A-Za-z0-9_]/; + +function isIdentChar(c: string | undefined): boolean { + return c !== undefined && (IDENT_CHAR.test(c) || c.charCodeAt(0) > 127); +} + +/** + * Rust source with its comments, and the contents of its strings, raw + * strings and character literals, blanked: offsets and newlines stay, so an + * offset still maps to its line and column. A lifetime (`'a`) is no + * character literal. + */ +export function blankRustCode(src: string): string { + const n = src.length; + const parts: string[] = []; + let last = 0; + // `src[from, to)` as spaces, its newlines kept; the text before it as it is. + const blank = (from: number, to: number): void => { + if (to <= from) return; + parts.push(src.slice(last, from)); + const chunk = src.slice(from, to); + parts.push(chunk.includes('\n') ? chunk.replace(/[^\n]/g, ' ') : ' '.repeat(to - from)); + last = to; + }; + const special = /[/"']/g; + for (let m = special.exec(src); m; m = special.exec(src)) { + const i = m.index; + const c = src[i]; + const d = src[i + 1]; + if (c === '/') { + if (d === '/') { + const end = src.indexOf('\n', i); + const to = end < 0 ? n : end; + blank(i, to); + special.lastIndex = to; + } else if (d === '*') { + let depth = 1; + let j = i + 2; + while (j < n && depth > 0) { + if (src[j] === '/' && src[j + 1] === '*') { depth++; j += 2; } + else if (src[j] === '*' && src[j + 1] === '/') { depth--; j += 2; } + else j++; + } + blank(i, j); + special.lastIndex = j; + } + continue; + } + if (c === '"') { + // A raw string: `r"…"`, `r#"…"#`, also after a `b` or `c` prefix — + // the `r` no end of a longer name. + let k = i - 1; + while (k >= 0 && src[k] === '#') k--; + let p = k - 1; + if (src[p] === 'b' || src[p] === 'c') p--; + if (src[k] === 'r' && (p < 0 || !isIdentChar(src[p]))) { + const close = `"${'#'.repeat(i - 1 - k)}`; + const end = src.indexOf(close, i + 1); + const to = end < 0 ? n : end; + blank(i + 1, to); + special.lastIndex = Math.min(n, to + close.length); + continue; + } + let j = i + 1; + while (j < n && src[j] !== '"') j += src[j] === '\\' ? 2 : 1; + blank(i + 1, Math.min(j, n)); + special.lastIndex = Math.min(n, j + 1); + continue; + } + // `'\n'`, `'\u{1F600}'`; `'x'`; a character outside the BMP; else a lifetime or a label. + if (d === '\\') { + const end = src.indexOf('\'', i + 3); + if (end > 0 && end - i <= 12) { + blank(i + 1, end); + special.lastIndex = end + 1; + } + } else if (src[i + 2] === '\'' && d !== undefined && d !== '\n') { + blank(i + 1, i + 2); + special.lastIndex = i + 3; + } else if (d !== undefined && /[\uD800-\uDBFF]/.test(d) && src[i + 3] === '\'') { + blank(i + 1, i + 3); + special.lastIndex = i + 4; + } + } + parts.push(src.slice(last)); + return parts.join(''); +} + +/** Each `(`, `[` and `{` → the offset of the bracket closing it. */ +function pairBrackets(code: string): Map { + const close = new Map(); + const stack: number[] = []; + const brackets = /[()[\]{}]/g; + for (let m = brackets.exec(code); m; m = brackets.exec(code)) { + const i = m.index; + const c = code[i]!; + if (c === '(' || c === '[' || c === '{') { + stack.push(i); + continue; + } + const want = c === ')' ? '(' : c === ']' ? '[' : '{'; + // A stray closer (a macro's unbalanced tokens) closes nothing. + if (stack.length > 0 && code[stack[stack.length - 1]!] === want) close.set(stack.pop()!, i); + } + return close; +} + +function skipSpace(code: string, at: number): number { + for (let c = code.charCodeAt(at); c === 32 || c === 10 || c === 13 || c === 9 || c === 12 || c === 11; c = code.charCodeAt(++at)); + return at; +} + +/** The identifier starting at `at`, if one does. */ +const IDENT_AT = /[A-Za-z_]\w*/y; +function identAt(code: string, at: number): string | null { + IDENT_AT.lastIndex = at; + return IDENT_AT.exec(code)?.[0] ?? null; +} + +/** The offset of the `>` closing the `<` at `at`, or -1. `->` and `=>` close nothing. */ +function angleClose(code: string, at: number, limit = code.length): number { + let depth = 0; + for (let i = at; i < limit; i++) { + const c = code[i]; + if (c === '<') depth++; + else if (c === '>' && code[i - 1] !== '-' && code[i - 1] !== '=') { + if (--depth === 0) return i; + } else if (c === ';' || c === '{' || c === '}') return -1; + } + return -1; +} + +/** + * Walk `code` from `from` to `to` over what lies outside brackets: `stop` + * sees each such offset — a bracket opening a group too — and a true return + * ends the walk there; past it, a `(`, `[` or `{` group is jumped over + * whole, and with `angles` so is a `<…>` one. Returns that offset, or `to`. + */ +function walkTop(code: string, from: number, to: number, close: Map, angles: boolean, stop: (i: number) => boolean): number { + let depth = 0; + for (let i = from; i < to; i++) { + const c = code[i]!; + if (depth === 0 && stop(i)) return i; + if (c === '(' || c === '[' || c === '{') { + const end = close.get(i); + if (end === undefined || end >= to) return to; + i = end; + continue; + } + if (angles && c === '<') depth++; + else if (angles && c === '>' && code[i - 1] !== '-' && code[i - 1] !== '=' && depth > 0) depth--; + } + return to; +} + +/** The spans of `code` between `from` and `to` that top-level commas part. */ +function splitTop(code: string, from: number, to: number, close: Map, angles: boolean): Array<[number, number]> { + const parts: Array<[number, number]> = []; + let start = from; + for (;;) { + const comma = walkTop(code, start, to, close, angles, (i) => code[i] === ','); + if (comma >= to) break; + parts.push([start, comma]); + start = comma + 1; + } + if (code.slice(start, to).trim() !== '') parts.push([start, to]); + return parts; +} + +/** A top-level `:` that is no part of a `::`, between `from` and `to`; -1 for none. */ +function topColon(code: string, from: number, to: number, close: Map): number { + const at = walkTop(code, from, to, close, true, (i) => code[i] === ':' && code[i + 1] !== ':' && code[i - 1] !== ':'); + return at < to ? at : -1; +} + +/** Whether the keyword `word` starts at `i`, a whole word. */ +function wordAt(code: string, i: number, word: string): boolean { + return code.startsWith(word, i) && !isIdentChar(code[i - 1]) && !isIdentChar(code[i + word.length]); +} + +/** Words a pattern writes that bind nothing. */ +const PATTERN_WORDS: ReadonlySet = new Set(['ref', 'mut', 'box', 'true', 'false', 'self', 'Self', 'crate', 'super', '_']); + +function isBindingName(id: string): boolean { + return /^[a-z_][A-Za-z0-9_]*$/.test(id) && !PATTERN_WORDS.has(id); +} + +/** A path at `at` — `Commands::Build`, `Self::A` — as its segments and the offset after it. */ +function readPath(code: string, at: number): { segments: string[]; end: number } { + const segments: string[] = []; + let p = at; + for (;;) { + const id = identAt(code, p); + if (!id) break; + segments.push(id); + p += id.length; + let q = skipSpace(code, p); + if (!code.startsWith('::', q)) break; + q = skipSpace(code, q + 2); + // A turbofish: `Foo::::Bar`, `Vec::`. + if (code[q] === '<') { + const end = angleClose(code, q); + if (end < 0) break; + p = end + 1; + q = skipSpace(code, p); + if (!code.startsWith('::', q)) break; + q = skipSpace(code, q + 2); + } + p = q; + } + return { segments, end: p }; +} + +type Emit = (name: string, at: number, site: RustPatternSite | undefined) => void; + +/** + * The names a pattern between `from` and `to` binds, each with the + * tuple-struct, variant or struct pattern it sits in, innermost first. + */ +function walkPattern(code: string, from: number, to: number, close: Map, site: RustPatternSite | undefined, emit: Emit): void { + let p = from; + while (p < to) { + const c = code[p]!; + if (c === '(' || c === '[') { + // A tuple or a slice: its fields' types are not read here. + const end = Math.min(close.get(p) ?? to, to); + for (const [a, b] of splitTop(code, p + 1, end, close, false)) walkPattern(code, a, b, close, undefined, emit); + p = end + 1; + continue; + } + if (!/[A-Za-z_]/.test(c) || isIdentChar(code[p - 1])) { + p++; + continue; + } + const path = readPath(code, p); + if (path.segments.length === 0) { + p++; + continue; + } + const after = skipSpace(code, path.end); + const written = path.segments.join('::'); + if (code[after] === '(') { + const end = Math.min(close.get(after) ?? to, to); + let index = 0; + for (const [a, b] of splitTop(code, after + 1, end, close, false)) { + walkPattern(code, a, b, close, { path: written, index }, emit); + index++; + } + p = end + 1; + continue; + } + if (code[after] === '{') { + const end = Math.min(close.get(after) ?? to, to); + for (const [a, b] of splitTop(code, after + 1, end, close, false)) { + const colon = topColon(code, a, b, close); + if (colon >= 0) { + const field = code.slice(a, colon).trim(); + if (/^\w+$/.test(field)) walkPattern(code, colon + 1, b, close, { path: written, field }, emit); + continue; + } + // `name`, `ref name`, `mut name`, `ref mut name` bind the field of that name. + const text = code.slice(a, b); + const short = /^\s*(?:ref\s+)?(?:mut\s+)?([A-Za-z_]\w*)\s*$/.exec(text); + if (short && isBindingName(short[1]!)) emit(short[1]!, a + text.lastIndexOf(short[1]!), { path: written, field: short[1]! }); + } + p = end + 1; + continue; + } + if (path.segments.length === 1 && isBindingName(path.segments[0]!)) emit(path.segments[0]!, p, site); + // `name @ pattern`: the subpattern binds in its own right. + p = code[after] === '@' ? after + 1 : path.end; + } +} + +/** The innermost `{…}` holding an offset, as [open, close]; null outside every block. */ +function blockAt(events: number[], open: number[], close: Map, codeLength: number, offset: number): [number, number] | null { + let lo = 0; + let hi = events.length - 1; + let at = -1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + if (events[mid]! < offset) { at = mid; lo = mid + 1; } else hi = mid - 1; + } + const o = at < 0 ? -1 : open[at]!; + return o < 0 ? null : [o, close.get(o) ?? codeLength]; +} + +/** + * Whether a `|` at `i` opens a closure's parameter list: it stands where an + * expression starts — after `(`, `[`, `{`, `,`, `=`, `;`, `:`, `=>`, or + * `move` / `return` / `async` — never after an operand, where it is an `|` + * of bits or a pattern's alternative. + */ +function opensClosure(code: string, i: number): boolean { + let k = i - 1; + while (k >= 0 && /\s/.test(code[k]!)) k--; + if (k < 0) return true; + const c = code[k]!; + if (c === ':') return code[k - 1] !== ':'; + if ('([{,=;'.includes(c)) return true; + if (c === '>') return code[k - 1] === '='; + const word = /([A-Za-z_]\w*)$/.exec(code.slice(Math.max(0, k - 8), k + 1))?.[1]; + return word === 'move' || word === 'return' || word === 'async' || word === 'static'; +} + +/** Where an expression starting at `from` ends: its first top-level `,` or `;`, or the bracket closing around it. */ +function expressionEnd(code: string, from: number, close: Map): number { + for (let i = from; i < code.length; i++) { + const c = code[i]!; + if (c === '(' || c === '[' || c === '{') { + const end = close.get(i); + if (end === undefined) return code.length; + i = end; + continue; + } + if (c === ',' || c === ';' || c === ')' || c === ']' || c === '}') return i; + } + return code.length; +} + +/** + * A Rust file read for its bindings: its blanked text, its functions with + * bodies, its `impl` and `trait` blocks. The bindings themselves are read on + * the first lookup — most files read here are only looked into. + */ +export function readRustScopes(source: string): RustScopes { + const code = blankRustCode(source); + const n = code.length; + const lineStarts = [0]; + for (let i = code.indexOf('\n'); i >= 0; i = code.indexOf('\n', i + 1)) lineStarts.push(i + 1); + const close = pairBrackets(code); + + const fns: RustFn[] = []; + for (const m of code.matchAll(/\bfn\b/g)) { + let p = skipSpace(code, m.index! + 2); + const name = identAt(code, p); + if (!name) continue; + p = skipSpace(code, p + name.length); + let generics = ''; + if (code[p] === '<') { + const end = angleClose(code, p); + if (end < 0) continue; + generics = code.slice(p + 1, end); + p = skipSpace(code, end + 1); + } + if (code[p] !== '(') continue; + const paramsEnd = close.get(p); + if (paramsEnd === undefined) continue; + // The body: the first `{` past the return type and `where` clause; a `;` + // first is a declaration, which binds nothing. + let body = -1; + let where = -1; + for (let q = paramsEnd + 1; q < n; q++) { + const c = code[q]!; + if (c === '{') { body = q; break; } + if (c === ';' || c === '}') break; + if (c === '(' || c === '[') { q = close.get(q) ?? n; continue; } + if (where < 0 && c === 'w' && wordAt(code, q, 'where')) where = q + 5; + } + if (body < 0) continue; + fns.push({ at: m.index!, generics, where: where < 0 ? '' : code.slice(where, body), from: p, to: close.get(body) ?? n, paramsEnd, body }); + } + + // `impl … {` and `trait Name: Super where … {` — an item, not + // the `impl Trait` of a type, which follows a `:`, `(`, `,`, `<`, `&` or `->`. + const impls: RustImplBlock[] = []; + for (const m of code.matchAll(/\b(impl|trait)\b/g)) { + const kind = m[1] as 'impl' | 'trait'; + if (kind === 'impl') { + let k = m.index! - 1; + while (k >= 0 && /\s/.test(code[k]!)) k--; + if (k >= 0 && !'{};]'.includes(code[k]!) && !/\b(?:unsafe|default)$/.test(code.slice(Math.max(0, k - 7), k + 1))) continue; + } + let p = skipSpace(code, m.index! + kind.length); + if (kind === 'trait') { + const name = identAt(code, p); + if (!name) continue; + p = skipSpace(code, p + name.length); + } + let generics = ''; + if (code[p] === '<') { + const end = angleClose(code, p); + if (end < 0) continue; + generics = code.slice(p + 1, end); + p = end + 1; + } + let where = -1; + let open = -1; + for (let q = p; q < n; q++) { + const c = code[q]!; + if (c === '{') { open = q; break; } + if (c === ';' || c === '}') break; + if (c === '(' || c === '[') { q = close.get(q) ?? n; continue; } + if (where < 0 && c === 'w' && wordAt(code, q, 'where')) where = q + 5; + } + if (open < 0) continue; + impls.push({ at: m.index!, kind, generics, where: where < 0 ? '' : code.slice(where, open), from: open, to: close.get(open) ?? n }); + } + + // `{` / `}` offsets, and the innermost block open after each. + const events: number[] = []; + const openAfter: number[] = []; + const stack: number[] = []; + const braces = /[{}]/g; + for (let m = braces.exec(code); m; m = braces.exec(code)) { + if (code.charCodeAt(m.index) === 123) stack.push(m.index); + else stack.pop(); + events.push(m.index); + openAfter.push(stack.length > 0 ? stack[stack.length - 1]! : -1); + } + + return { code, lineStarts, fns, impls, close, events, openAfter, bindings: null }; +} + +/** The innermost `{…}` of a file read by readRustScopes holding `offset`, as [open, close]; null outside every block. */ +export function rustInnerBlockAt(scopes: RustScopes, offset: number): [number, number] | null { + return blockAt(scopes.events, scopes.openAfter, scopes.close, scopes.code.length, offset); +} + +/** The names a Rust file binds (RustBinding), read from what readRustScopes kept of it. */ +function readRustBindings(scopes: RustScopes): Map { + const { code, close, events, openAfter } = scopes; + const n = code.length; + const bindings = new Map(); + const add = (name: string, binding: RustBinding): void => { + const list = bindings.get(name); + if (list) list.push(binding); + else bindings.set(name, [binding]); + }; + const bindPattern = (from: number, to: number, kind: RustBinding['kind'], scopeFrom: number, scopeTo: number, scrutinee?: string): void => { + walkPattern(code, from, to, close, undefined, (name, at, site) => + add(name, { at, kind, from: scopeFrom, to: scopeTo, site, scrutinee })); + }; + // A parameter `pattern: Type`, or a closure's `pattern`. + const param = (from: number, to: number, kind: 'param' | 'closure', scopeFrom: number, scopeTo: number): void => { + const colon = topColon(code, from, to, close); + const pattern = code.slice(from, colon < 0 ? to : colon); + const simple = /^\s*(?:mut\s+)?([A-Za-z_]\w*)\s*$/.exec(pattern); + if (simple) { + if (isBindingName(simple[1]!)) { + add(simple[1]!, { + at: from + pattern.lastIndexOf(simple[1]!), kind, from: scopeFrom, to: scopeTo, + type: colon < 0 ? undefined : code.slice(colon + 1, to).trim(), + }); + } + return; + } + bindPattern(from, colon < 0 ? to : colon, kind, scopeFrom, scopeTo); + }; + + for (const f of scopes.fns) { + for (const [a, b] of splitTop(code, f.from + 1, f.paramsEnd, close, true)) param(a, b, 'param', f.body, f.to); + } + + for (let i = code.indexOf('|'); i >= 0; i = code.indexOf('|', i + 1)) { + // `||` — no parameters, or an `or` — and `|=`. + if (code[i + 1] === '|') { i++; continue; } + if (code[i + 1] === '=' || !opensClosure(code, i)) continue; + let end = -1; + let depth = 0; + for (let j = i + 1; j < n && j < i + 2000; j++) { + const c = code[j]!; + if (c === '(' || c === '[' || c === '{') { + const e = close.get(j); + if (e === undefined) break; + j = e; + continue; + } + if (c === '<') depth++; + else if (c === '>' && code[j - 1] !== '-' && code[j - 1] !== '=') depth--; + else if (c === '|' && depth <= 0) { end = j; break; } + else if (c === ';' || c === ')' || c === ']' || c === '}') break; + } + if (end < 0) continue; + const b = skipSpace(code, end + 1); + let bodyEnd: number; + if (code.startsWith('->', b)) { + const brace = code.indexOf('{', b); + bodyEnd = brace < 0 ? n : close.get(brace) ?? n; + } else if (code[b] === '{') { + bodyEnd = close.get(b) ?? n; + } else { + bodyEnd = expressionEnd(code, b, close); + } + for (const [s, e] of splitTop(code, i + 1, end, close, true)) param(s, e, 'closure', end + 1, bodyEnd); + // The body is read on: closures nest. + i = end; + } + + for (const m of code.matchAll(/\blet\b/g)) { + const at = m.index!; + const cond = /(?:\bif|\bwhile|&&)\s*$/.test(code.slice(Math.max(0, at - 16), at)); + // The pattern: up to a top-level `=` (no `==` or `=>`), a `:` (no `::`), or a `;`. + const patFrom = at + 3; + const patEnd = walkTop(code, patFrom, n, close, false, (i) => { + const c = code[i]; + if (c === ';' || c === '}') return true; + if (c === ':') return code[i + 1] !== ':' && code[i - 1] !== ':'; + return c === '=' && code[i + 1] !== '=' && code[i + 1] !== '>' && !'=!<>'.includes(code[i - 1]!); + }); + if (patEnd >= n) continue; + let type: string | undefined; + let eq = code[patEnd] === '=' ? patEnd : -1; + if (code[patEnd] === ':') { + const typeEnd = walkTop(code, patEnd + 1, n, close, true, (i) => code[i] === ';' || code[i] === '}' || + (code[i] === '=' && code[i + 1] !== '=' && code[i + 1] !== '>' && !'=!<>'.includes(code[i - 1]!))); + if (typeEnd >= n) continue; + type = code.slice(patEnd + 1, typeEnd).trim(); + if (code[typeEnd] === '=') eq = typeEnd; + } + const simple = /^\s*(?:ref\s+)?(?:mut\s+)?([A-Za-z_]\w*)\s*$/.exec(code.slice(patFrom, patEnd)); + if (cond) { + if (eq < 0) continue; + // The value is matched up to the body's `{` or the next condition of a + // chain, which (with the body) sees what the pattern binds; the value + // itself sees the names around it. + const valueEnd = walkTop(code, eq + 1, n, close, false, (i) => code[i] === '{' || code[i] === ';' || (code[i] === '&' && code[i + 1] === '&')); + const scrutinee = code.slice(eq + 1, valueEnd).trim(); + const open = code[valueEnd] === '{' ? valueEnd : walkTop(code, valueEnd, n, close, false, (i) => code[i] === '{' || code[i] === ';'); + if (code[open] !== '{') continue; + bindPattern(patFrom, patEnd, 'cond', valueEnd, close.get(open) ?? n, scrutinee); + continue; + } + // A statement: up to its `;`, past a `let … else { … }`'s block. + let initEnd = n; + let stmtEnd = n; + if (eq >= 0) { + const stop = walkTop(code, eq + 1, n, close, false, (i) => code[i] === ';' || code[i] === '}' || + (code[i] === 'e' && wordAt(code, i, 'else') && !/\}\s*$/.test(code.slice(Math.max(0, i - 64), i)))); + initEnd = stop; + if (code[stop] === 'e') { + const open = skipSpace(code, stop + 4); + const elseEnd = code[open] === '{' ? close.get(open) ?? n : n; + const semi = skipSpace(code, elseEnd + 1); + stmtEnd = code[semi] === ';' ? semi : elseEnd; + } else { + stmtEnd = stop; + } + } else { + stmtEnd = walkTop(code, patEnd, n, close, false, (i) => code[i] === ';' || code[i] === '}'); + } + const block = blockAt(events, openAfter, close, n, at); + if (!block) continue; + const scopeFrom = stmtEnd + 1; + const scopeTo = block[1]; + const init = eq >= 0 ? code.slice(eq + 1, initEnd).trim() : undefined; + if (simple) { + if (isBindingName(simple[1]!)) { + add(simple[1]!, { + at: patFrom + code.slice(patFrom, patEnd).lastIndexOf(simple[1]!), kind: 'let', from: scopeFrom, to: scopeTo, + type, init: type === undefined ? init : undefined, + }); + } + continue; + } + bindPattern(patFrom, patEnd, 'let', scopeFrom, scopeTo, init); + } + + for (const m of code.matchAll(/\bfor\b/g)) { + const p = skipSpace(code, m.index! + 3); + // `for<'a>` binds lifetimes; `impl Trait for Type {` no names. + if (code[p] === '<') continue; + const inAt = walkTop(code, p, n, close, false, (i) => code[i] === '{' || code[i] === ';' || code[i] === '=' || + (code[i] === 'i' && wordAt(code, i, 'in'))); + if (code[inAt] !== 'i') continue; + const open = walkTop(code, inAt + 2, n, close, false, (i) => code[i] === '{' || code[i] === ';'); + if (code[open] !== '{') continue; + bindPattern(p, inAt, 'for', open, close.get(open) ?? n); + } + + for (const m of code.matchAll(/\bmatch\b/g)) { + const from = m.index! + 5; + const open = walkTop(code, from, n, close, false, (i) => code[i] === '{' || code[i] === ';' || code[i] === '}'); + if (code[open] !== '{') continue; + const scrutinee = code.slice(from, open).trim(); + if (scrutinee === '') continue; + const end = close.get(open) ?? n; + let armStart = open + 1; + for (let p = open + 1; p < end; p++) { + const c = code[p]!; + if (c === '(' || c === '[' || c === '{') { + p = close.get(p) ?? end; + continue; + } + if (c !== '=' || code[p + 1] !== '>') continue; + // The pattern, without its guard; the guard sees what it binds. + const guard = walkTop(code, armStart, p, close, false, (i) => code[i] === 'i' && wordAt(code, i, 'if')); + let q = skipSpace(code, p + 2); + let armEnd: number; + if (code[q] === '{') { + armEnd = close.get(q) ?? end; + q = skipSpace(code, armEnd + 1); + if (code[q] === ',') q++; + else q = armEnd + 1; + } else { + armEnd = walkTop(code, q, end, close, false, (i) => code[i] === ','); + q = armEnd + 1; + } + bindPattern(armStart, guard, 'arm', guard, armEnd, scrutinee); + armStart = q; + p = q - 1; + } + } + + return bindings; +} + +/** The innermost `impl` or `trait` body around `offset`, if any. */ +export function rustImplAt(scopes: RustScopes, offset: number): RustImplBlock | undefined { + let best: RustImplBlock | undefined; + for (const b of scopes.impls) { + if (offset > b.from && offset < b.to && (!best || b.from > best.from)) best = b; + } + return best; +} + +/** `text` parted at its top-level `sep`s, outside `()`, `[]`, `{}` and `<>` (`->` closes nothing). */ +export function splitRustTopLevel(text: string, sep: string): string[] { + const parts: string[] = []; + let depth = 0; + let start = 0; + for (let i = 0; i < text.length; i++) { + const c = text[i]!; + if (c === '(' || c === '[' || c === '{' || c === '<') depth++; + else if (c === ')' || c === ']' || c === '}' || (c === '>' && text[i - 1] !== '-' && text[i - 1] !== '=')) depth--; + else if (c === sep && depth === 0) { + parts.push(text.slice(start, i)); + start = i + 1; + } + } + parts.push(text.slice(start)); + return parts.map((p) => p.trim()).filter((p) => p !== ''); +} + +/** + * The bounds a generic parameter `name` is declared with, read from the + * generics lists (`T: Read + Send, U`) and `where` clauses (`T: Into`) of + * the items around it — a function's and its `impl`'s: each bound's text, + * `?Sized` and lifetimes left out. Undefined when no list declares `name`. + */ +export function rustBoundsOf(name: string, items: ReadonlyArray<{ generics: string; where: string }>): string[] | undefined { + let declared = false; + const bounds: string[] = []; + const take = (text: string): void => { + for (const b of splitRustTopLevel(text, '+')) { + const bound = b.replace(/^for\s*<[^>]*>\s*/, '').trim(); + if (bound === '' || bound.startsWith('?') || bound.startsWith('\'')) continue; + bounds.push(bound); + } + }; + for (const item of items) { + for (const part of splitRustTopLevel(item.generics, ',')) { + const m = /^([A-Za-z_]\w*)\s*(?::([\s\S]*?))?(?:=[\s\S]*)?$/.exec(part); + if (!m || m[1] !== name) continue; + declared = true; + if (m[2]) take(m[2]); + } + } + if (!declared) return undefined; + for (const item of items) { + for (const part of splitRustTopLevel(item.where, ',')) { + const m = /^([A-Za-z_]\w*)\s*:([\s\S]*)$/.exec(part); + if (m && m[1] === name) take(m[2]!); + } + } + return bounds; +} + +/** + * The binding of `name` in scope at `offset`: the innermost, latest one. A + * name an alternative pattern binds more than once there (`A(x) | B(x)`) + * comes back as `ambiguous`. + */ +export function rustBindingAt(scopes: RustScopes, name: string, offset: number): { binding: RustBinding; ambiguous: boolean } | undefined { + const list = (scopes.bindings ??= readRustBindings(scopes)).get(name); + if (!list) return undefined; + let best: RustBinding | undefined; + for (const b of list) { + if (offset >= b.from && offset < b.to && (!best || b.at > best.at)) best = b; + } + if (!best) return undefined; + const ambiguous = list.some((b) => b !== best && b.from === best!.from && b.to === best!.to && b.kind === best!.kind && + (b.kind === 'arm' || b.kind === 'cond' || b.kind === 'let')); + return { binding: best, ambiguous }; +} + +/** The innermost function with a body around `offset`, if any. */ +export function rustFnAt(scopes: RustScopes, offset: number): RustFn | undefined { + let best: RustFn | undefined; + for (const f of scopes.fns) { + if (offset >= f.from && offset <= f.to && (!best || f.from > best.from)) best = f; + } + return best; +}