fix(rust): a call on a local reaches the method of the type it is declared with (#2715)

A Rust method call on a local variable or a parameter (`opts.build()`) was
matched by the method's name whenever the receiver's declaration wasn't one
of two unscoped line patterns. dioxus's `Commands::Build(opts) =>
opts.build()` lost its link once the build module's `BuildRequest::build`
shared the name, and calls on std or dependency values linked to project
namesakes: `cx.waker()` on a `Context` to tokio's `CachedParkThread::waker`.

The receiver is now typed by the binding the call sees. The new
src/resolution/rust-scopes.ts reads each file's parameters, closure
parameters, lets, if-let / while-let / let-else, for and match-arm bindings
with block scope. A binding's type is its written one, the value it is
built from, or the variant or struct field its pattern takes. The method is
the one Rust picks on that type: inherent first, then a trait impl whose
trait is in scope, then a trait's provided method (a blanket impl only where
its bounds are met), then Deref. `&`, Box, Rc, Arc, Pin, Cow and the lock
guards are looked through, their own methods kept (one list with the
self-call path's), and a project impl on the wrapper itself answers before
what the wrapper holds, as it does for `self`. `impl Trait`, `dyn Trait` and
bounded generics reach the trait's method or a supertrait's. Among types
sharing a name, the one its path names through the project's modules wins,
the path written or bound by a block-scoped `use`, else the one that has
the method. A std, dependency or primitive type reaches only a project trait
impl in scope, and not one that hands the call on to the type's own method
of that name.

A binding that writes no type leaves the call to the name strategies, which
no longer take a project type from another declaration of the name. A
trait's own methods carry no `pub`: isVisibleAcrossFiles lets them through
for the calls this path resolved, and name guesses stay out as before.

A/B with fresh indexes against main on axum, serde, ripgrep, clap, dioxus,
tokio and gleam, every changed edge triaged: 4,645 calls linked that were
not, 887 removed (calls on std or dependency values, project types without
the method, a wrapper's own methods), 459 moved to the declared type's
method, 3,669 kept at a higher confidence. gin, serilog, flask and express
are byte-identical. The new code costs 0.6 to 0.9 s of CPU per full index
of tokio, dioxus or gleam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-20 18:55:06 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent f5158fac02
commit 23b5ca1b43
5 changed files with 2783 additions and 9 deletions
+1
View File
@@ -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 `<T as Trait>::Name`, or bound in a trait's arguments like `Iterator<Item = &str>`, now links only to what it can name: through `<Self as Trait>` 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 `<L as Listener>::Io` linked to one listener's `type Io`, its `Iterator<Item = &str>` 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.
+721
View File
@@ -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<string, string> = {
'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<FormData>) {',
' 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<Handle> {',
' fn schedule(&self, task: u32) {}',
'}',
'',
'pub fn run(handle: Arc<Handle>) {',
' 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<String> },',
' 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<T> {',
' pub value: T,',
'}',
'',
'pub type TypedStatement = Statement<u32>;',
'pub type UntypedStatement = Statement<String>;',
'',
'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<T> {',
' Leaf(Statement<T>),',
'}',
'',
'pub fn typed(node: &Node<u32>) {',
' 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<dyn netlib::Runtime> {',
' 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<BuildArgs>),',
' 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<T> {',
' pub inner: T,',
'}',
'',
'impl Overrides<BuildArgs> {',
' 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>) {',
' server.run();',
'}',
'',
'pub fn boxed(worker: Box<Worker>) {',
' worker.run();',
'}',
'',
'pub fn counted(worker: &mut Rc<Worker>) {',
' 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<Server> = 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<dyn Render>) {',
' 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<Worker> {',
' 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<BuildArgs>`, 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<dyn Trait>`', () => {
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<T>` 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<dyn Any>` 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']);
});
});
+2 -2
View File
@@ -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', () => {
File diff suppressed because it is too large Load Diff
+779
View File
@@ -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<number, number>;
/** 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<string, RustBinding[]> | 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<number, number> {
const close = new Map<number, number>();
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<number, number>, 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<number, number>, 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, number>): 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<string> = 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::<T>::Bar`, `Vec::<u8>`.
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<number, number>, 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<number, number>, 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, number>): 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<T: Bound> … {` and `trait Name<T>: 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<string, RustBinding[]> {
const { code, close, events, openAfter } = scopes;
const n = code.length;
const bindings = new Map<string, RustBinding[]>();
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<U>`) 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;
}