mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 12:00:02 +08:00
fix(rust): a for-loop variable or a tuple pattern's name has the type it is bound from (#2717)
A Rust `for` loop's variable, and a name a tuple pattern binds (`for (k, v) in &map`, `let (tx, rx) = mpsc::channel(8)`), had no type the typed receiver path could read, so calls on them fell to the older receiver inference and the name strategies. tokio's 198 `tx.send(…)` calls on channel senders went to the internal `chan::Tx::send`, and 100 calls on the receivers to `chan::Rx`. The scope reader now records what a `for` loop iterates, and gives the plain names of a top-level tuple pattern their place in it. A loop's variable is an item of what it iterates: a std sequence's element, a std map's `(K, V)`, a slice's, a std iterator's or an `impl Iterator<Item = T>`'s item, through `&` and the pointers that deref to what they hold. The std adapters that keep the items (`.iter()`, `.rev()`, `.skip(n)`) or project them (`.keys()`, `.values()`, `.enumerate()`) are looked through only where what they are called on is read, so a project method of one of those names is read from its own signature. A tuple pattern's name is the matched tuple's field at its place. A field access (`self.items`, `opts.config`, `pair.0`) has the type the receiver's struct declares for the field. A free function called through a module that a `use … as` renames was looked up by the alias: tokio's loom tests' `oneshot::channel()` under `use crate::runtime::tests::loom_oneshot as oneshot` read the return type of `sync::oneshot::channel`. The path's head now goes through its `use`, and the project's modules are walked along it. A/B with fresh indexes against main on axum, serde, ripgrep, clap, dioxus, tokio and gleam, every changed edge triaged: 394 calls linked that were not, 86 removed (calls on std or dependency values, tokio values whose read and write methods come from macro-declared extension traits, derived `Clone`s, fields that hold functions), 404 moved to the declared type's method, 625 kept at a higher confidence. gin, serilog, flask and express are byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e043efbc84
commit
4286083a84
@@ -239,6 +239,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- 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 Rust, a variable that holds what a method call returns, like `let server = builder.port(80).build();`, `let settings = config.load()?;` or `let conn = client.connect().await;`, now has the type that method is declared to return, so calls on the variable link to that type's methods. A generic method's type comes from the call's turbofish, like `arg.get::<ValueHint>()`, and a call on a type that implements one trait twice, like `Encoder<Bytes>` and `Encoder<&[u8]>`, links to the implementation that takes the argument given. Before, such calls were linked by name and could reach another type's method of the same name. Re-index Rust projects after upgrading.
|
||||
- In Rust, a `for` loop's variable now has the type of an item of what the loop goes over, like `for item in &self.items` over a `Vec<Item>`, `for (key, value) in &map` over a `HashMap`, or `for sc in cmd.get_subcommands()` over the `impl Iterator` a method returns, and each name in a tuple pattern like `let (tx, rx) = mpsc::channel(8);` has the type of its part of the tuple, so calls on them link to that type's methods. Before, such calls were linked by name: tokio's `tx.send(…)` calls on channel senders went to an internal type's `send`. Re-index Rust projects after upgrading.
|
||||
- In C++, every name a declaration lists now has the declaration's type, like `strings_found` in google/breakpad's `ByteBuffer symbols_found, strings_found;` or `data_` in a class's `Table *index_, *data_;`, so calls on it link to that class's methods. Before, only the first name had its type read, and a call on another name linked to nothing or to the class of an earlier variable with the same name.
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* A Rust `for` loop's variable is an item of what the loop iterates — a
|
||||
* field's `Vec<Item>`, a `HashMap<K, V>`'s `(K, V)`, `.enumerate()`'s
|
||||
* `(usize, T)`, a method's `impl Iterator<Item = T>` — and a name a tuple
|
||||
* pattern binds (`for (k, v) in …`, `let (a, b) = …`) is the tuple's field at
|
||||
* its place, so a call on either reaches that type's method, not a
|
||||
* same-named method of another type.
|
||||
*/
|
||||
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;
|
||||
|
||||
/** Two modules of one shape: tokio's `sync::oneshot` and the loom tests' `loom_oneshot`. */
|
||||
const oneshotModule = [
|
||||
'pub struct Sender;',
|
||||
'',
|
||||
'impl Sender {',
|
||||
' pub fn send(&self) {}',
|
||||
'}',
|
||||
'',
|
||||
'pub struct Receiver;',
|
||||
'',
|
||||
'impl Receiver {',
|
||||
' pub fn recv(&self) {}',
|
||||
'}',
|
||||
'',
|
||||
'pub fn channel() -> (Sender, Receiver) {',
|
||||
' (Sender, Receiver)',
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-loop-tuple-'));
|
||||
const files: Record<string, string> = {
|
||||
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n',
|
||||
'src/main.rs': 'mod loom;\nmod model;\nmod oneshot;\nmod loom_oneshot;\n\nfn main() {}\n',
|
||||
'src/oneshot.rs': oneshotModule,
|
||||
'src/loom_oneshot.rs': oneshotModule,
|
||||
'src/loom.rs': [
|
||||
'use crate::loom_oneshot as oneshot;',
|
||||
'',
|
||||
'pub fn run() {',
|
||||
' let (tx, rx) = oneshot::channel();',
|
||||
' tx.send();',
|
||||
' rx.recv();',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'src/model.rs': [
|
||||
'pub struct Item;',
|
||||
'',
|
||||
'impl Item {',
|
||||
' pub fn price(&self) -> u32 {',
|
||||
' 0',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'pub struct Key;',
|
||||
'',
|
||||
'impl Key {',
|
||||
' pub fn label(&self) {}',
|
||||
'}',
|
||||
'',
|
||||
'// Same-file namesakes, which a guess by name alone may take.',
|
||||
'pub struct Decoy;',
|
||||
'',
|
||||
'impl Decoy {',
|
||||
' pub fn price(&self) -> u32 {',
|
||||
' 0',
|
||||
' }',
|
||||
' pub fn label(&self) {}',
|
||||
'}',
|
||||
'',
|
||||
'pub struct Store {',
|
||||
' pub items: Vec<Item>,',
|
||||
' pub index: std::collections::HashMap<Key, Item>,',
|
||||
' pub pair: (Key, Item),',
|
||||
'}',
|
||||
'',
|
||||
'impl Store {',
|
||||
' pub fn all(&self) -> impl Iterator<Item = &Item> {',
|
||||
' self.items.iter()',
|
||||
' }',
|
||||
' pub fn total(&self) -> u32 {',
|
||||
' let mut sum = 0;',
|
||||
' for item in &self.items {',
|
||||
' sum += item.price();',
|
||||
' }',
|
||||
' sum',
|
||||
' }',
|
||||
' pub fn entries(&self) {',
|
||||
' for (key, item) in &self.index {',
|
||||
' key.label();',
|
||||
' item.price();',
|
||||
' }',
|
||||
' }',
|
||||
' pub fn numbered(&self) {',
|
||||
' for (i, item) in self.items.iter().enumerate() {',
|
||||
' item.price();',
|
||||
' }',
|
||||
' }',
|
||||
' pub fn keys(&self) {',
|
||||
' for key in self.index.keys() {',
|
||||
' key.label();',
|
||||
' }',
|
||||
' }',
|
||||
' pub fn walk(&self) {',
|
||||
' for item in self.all() {',
|
||||
' item.price();',
|
||||
' }',
|
||||
' }',
|
||||
' pub fn split(&self) {',
|
||||
' let (key, item) = &self.pair;',
|
||||
' key.label();',
|
||||
' item.price();',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'fn make_pair() -> (Key, Item) {',
|
||||
' (Key, Item)',
|
||||
'}',
|
||||
'',
|
||||
'pub fn unpack() {',
|
||||
' let (key, item) = make_pair();',
|
||||
' key.label();',
|
||||
' item.price();',
|
||||
'}',
|
||||
'',
|
||||
'pub fn shadowed(store: Store, item: Decoy) {',
|
||||
' item.label();',
|
||||
' for item in store.index.values() {',
|
||||
' item.price();',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].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 `for` loop variable', () => {
|
||||
it('is an item of the field the loop iterates', () => {
|
||||
expect(callsFrom('src/model.rs', 'total')).toEqual(['src/model.rs@4']);
|
||||
});
|
||||
|
||||
it('is a map\'s key and value in a `(k, v)` pattern', () => {
|
||||
expect(callsFrom('src/model.rs', 'entries')).toEqual(['src/model.rs@12', 'src/model.rs@4']);
|
||||
});
|
||||
|
||||
it('is numbered by `.enumerate()`', () => {
|
||||
expect(callsFrom('src/model.rs', 'numbered')).toEqual(['src/model.rs@4']);
|
||||
});
|
||||
|
||||
it('is a map\'s key through `.keys()`', () => {
|
||||
expect(callsFrom('src/model.rs', 'keys')).toEqual(['src/model.rs@12']);
|
||||
});
|
||||
|
||||
it('is the item a method\'s `impl Iterator` hands out', () => {
|
||||
expect(callsFrom('src/model.rs', 'walk')).toEqual(['src/model.rs@32', 'src/model.rs@4']);
|
||||
});
|
||||
|
||||
it('hides a parameter of its name, and is a map\'s value through `.values()`', () => {
|
||||
// `item.label()` on the `Decoy` parameter, then `Item::price` in the loop.
|
||||
expect(callsFrom('src/model.rs', 'shadowed')).toEqual(['src/model.rs@22', 'src/model.rs@4']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('a name a Rust tuple pattern binds', () => {
|
||||
it('is the field of a tuple-typed field at its place', () => {
|
||||
expect(callsFrom('src/model.rs', 'split')).toEqual(['src/model.rs@12', 'src/model.rs@4']);
|
||||
});
|
||||
|
||||
it('is the field of the tuple the function a `use … as` alias names returns', () => {
|
||||
// `oneshot::channel()` under `use crate::loom_oneshot as oneshot` is
|
||||
// `loom_oneshot`'s, so `tx.send()` and `rx.recv()` are its types'. (The
|
||||
// path call itself is the import resolver's to link.)
|
||||
const calls = callsFrom('src/loom.rs', 'run');
|
||||
expect(calls).toContain('src/loom_oneshot.rs@4');
|
||||
expect(calls).toContain('src/loom_oneshot.rs@10');
|
||||
expect(calls).not.toContain('src/oneshot.rs@4');
|
||||
expect(calls).not.toContain('src/oneshot.rs@10');
|
||||
});
|
||||
|
||||
it('is the field of a function\'s returned tuple at its place', () => {
|
||||
expect(callsFrom('src/model.rs', 'unpack')).toEqual(['src/model.rs@12', 'src/model.rs@4', 'src/model.rs@70']);
|
||||
});
|
||||
});
|
||||
@@ -9808,6 +9808,12 @@ function rustGenericBounds(name: string, written: RustWrittenType, context: Reso
|
||||
/** 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.kind === 'for') {
|
||||
// `for x in …`: an element of what the loop iterates; `for (k, v) in …` a field of that element.
|
||||
const element = binding.scrutinee === undefined ? null : rustIteratedElement(binding.scrutinee, binding.at, scopes, ref, context, depth);
|
||||
if (!element || !binding.site) return element;
|
||||
return binding.site.path === '' && binding.site.index !== undefined ? rustTupleField(element, binding.site.index) : null;
|
||||
}
|
||||
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;
|
||||
@@ -9896,6 +9902,9 @@ function rustValueType(value: string, at: number, scopes: RustScopes, ref: Unres
|
||||
const method = /^([\s\S]+?)\s*\.\s*([a-z_]\w*)\s*(?:::\s*<([^()]*)>)?$/.exec(call.callee);
|
||||
return method ? rustMethodReturn(method[1]!, method[2]!, method[3], unwraps, awaits, at, scopes, ref, context, depth) : null;
|
||||
}
|
||||
// `self.items`, `opts.config.port`, `pair.0`: the field's type as its struct declares it.
|
||||
const access = /^([\s\S]+?)\s*\.\s*([A-Za-z_]\w*|\d+)$/.exec(v);
|
||||
if (access && !/^\d/.test(v)) return unwrapped(rustFieldValue(access[1]!, access[2]!, at, scopes, ref, context, depth));
|
||||
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;
|
||||
@@ -9977,10 +9986,27 @@ function rustFunctionReturn(modules: string[], fn: string, unwraps: number, at:
|
||||
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));
|
||||
// The path the call writes, its head as a `use` binds it (`oneshot::channel()`
|
||||
// under tokio's `use crate::runtime::tests::loom_oneshot as oneshot`), or
|
||||
// the one a `use` binds the bare name to.
|
||||
let path: string[];
|
||||
if (modules.length === 0) {
|
||||
const used = rustUsePath(fn, ref.filePath, at, context);
|
||||
if (!used) return null;
|
||||
path = used;
|
||||
} else {
|
||||
const head = rustUsePath(modules[0]!, ref.filePath, at, context);
|
||||
path = [...(head && head.length > 0 ? head : [modules[0]!]), ...modules.slice(1), fn];
|
||||
}
|
||||
// The function the project's modules walk to, else the one whose file's modules the path names.
|
||||
const walked = rustWalkedItems(path, ref.filePath, at, context);
|
||||
const named = walked === null ? [] : fns.filter((n) => walked.has(n.id));
|
||||
if (named.length > 0) {
|
||||
fns = named;
|
||||
} else {
|
||||
const segments = path.slice(0, -1).filter((s) => s !== 'crate' && s !== 'self' && s !== 'super' && !rustProjectCrates(context).has(s));
|
||||
if (segments.length > 0) fns = fns.filter((n) => rustModulesLead(segments, n));
|
||||
}
|
||||
}
|
||||
const returns = new Set(fns.map((f) => rustReturnText(f.signature) ?? ''));
|
||||
if (fns.length === 0 || returns.size !== 1) return null;
|
||||
@@ -10024,7 +10050,7 @@ function readRustMethodReturn(
|
||||
let written: RustWrittenType | null;
|
||||
if (recv === 'self') written = { text: 'Self', file: ref.filePath, at };
|
||||
else if (/^[a-z_]\w*$/.test(recv)) written = rustLocalType(recv, at, scopes, ref, context, depth);
|
||||
else if (/[)?]$/.test(recv)) written = rustValueType(recv, at, scopes, ref, context, depth + 1);
|
||||
else if (/[)?]$|\.\s*(?:[A-Za-z_]\w*|\d+)$/.test(recv)) written = rustValueType(recv, at, scopes, ref, context, depth + 1);
|
||||
else return null;
|
||||
if (!written) return null;
|
||||
const read = rustReadType(written, ref, context);
|
||||
@@ -10121,6 +10147,153 @@ function rustFutureOutput(written: RustWrittenType): RustWrittenType | null {
|
||||
return output ? { ...written, text: output.trim() } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a Rust field access is — `self.items`, `opts.config`, `pair.0` — as
|
||||
* the receiver's struct declares the field, read where the struct is written
|
||||
* (its own generics opaque, the field's type being whatever a value of it
|
||||
* makes them). Null when the receiver isn't one struct of the project's.
|
||||
*/
|
||||
function rustFieldValue(
|
||||
receiver: string, field: string, at: number, scopes: RustScopes, ref: UnresolvedRef, context: ResolutionContext, depth: number,
|
||||
): RustWrittenType | null {
|
||||
const recv = receiver.trim();
|
||||
let written: RustWrittenType | null;
|
||||
if (recv === 'self') written = { text: 'Self', file: ref.filePath, at };
|
||||
else if (/^[a-z_]\w*$/.test(recv)) written = rustLocalType(recv, at, scopes, ref, context, depth);
|
||||
else written = rustValueType(recv, at, scopes, ref, context, depth + 1);
|
||||
if (!written) return null;
|
||||
const read = rustReadType(written, ref, context);
|
||||
if (read.kind !== 'named') return null;
|
||||
const lookup = rustTypeLookup(read.name, read.path, read.written.file, context, read.written.at);
|
||||
const structs = lookup.outside ? [] : lookup.nodes.filter((n) => n.kind === 'struct' || n.kind === 'union');
|
||||
if (structs.length !== 1) return null;
|
||||
const decl = structs[0]!;
|
||||
const fields = rustDeclaredFields(decl, context);
|
||||
const text = /^\d+$/.test(field) ? fields?.tuple?.[Number(field)] : fields?.named?.get(field);
|
||||
if (!fields || !text) return null;
|
||||
return { text, file: decl.filePath, at: rustNodeOffset(decl, context), self: decl.name, generics: fields.generics };
|
||||
}
|
||||
|
||||
/** The `index`th field of a Rust tuple type as written — `(K, V)` → `V` for 1 — through `&`; null for any other type. */
|
||||
function rustTupleField(written: RustWrittenType, index: number): RustWrittenType | null {
|
||||
const t = written.text.trim().replace(/^(?:&\s*(?:'\w+\s+)?(?:mut\s+)?)+/, '');
|
||||
if (!t.startsWith('(') || !rustGroupIsWhole(t)) return null;
|
||||
const fields = splitRustTopLevel(t.slice(1, -1), ',').map((f) => f.trim()).filter((f) => f !== '');
|
||||
return index < fields.length ? { ...written, text: fields[index]!, wrappers: undefined } : null;
|
||||
}
|
||||
|
||||
/** Std adapters that hand on the items of what they are called on. */
|
||||
const RUST_ITEM_ADAPTERS: ReadonlySet<string> = new Set([
|
||||
'iter', 'iter_mut', 'into_iter', 'drain', 'rev', 'copied', 'cloned', 'skip', 'take', 'step_by', 'peekable', 'by_ref', 'fuse',
|
||||
]);
|
||||
/** Std adapters that pick a map's keys or values, or number the items. */
|
||||
const RUST_ITEM_PROJECTIONS: ReadonlyMap<string, 'key' | 'value' | 'enumerate'> = new Map([
|
||||
['keys', 'key'], ['into_keys', 'key'], ['values', 'value'], ['values_mut', 'value'], ['into_values', 'value'], ['enumerate', 'enumerate'],
|
||||
]);
|
||||
|
||||
/**
|
||||
* What a Rust `for` loop binds, an item of what it iterates: `for a in
|
||||
* cmd.get_arguments()` with `-> impl Iterator<Item = &Arg>` an `Arg`, `for x
|
||||
* in &self.items` with `items: Vec<Item>` an `Item`, `for (i, v) in
|
||||
* map.values().enumerate()` a `(usize, V)`. The std adapters that keep the
|
||||
* items (`.iter()`, `.rev()`, `.skip(n)`) or project them (`.values()`,
|
||||
* `.enumerate()`) are looked through only where the value they are called on
|
||||
* is read: a project method of the name is read from its own signature.
|
||||
*/
|
||||
function rustIteratedElement(iter: string, at: number, scopes: RustScopes, ref: UnresolvedRef, context: ResolutionContext, depth: number): RustWrittenType | null {
|
||||
let v = iter.trim();
|
||||
const steps: string[] = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const value = rustValueType(v.replace(/^&\s*(?:mut\s+)?/, ''), at, scopes, ref, context, depth + 1);
|
||||
if (value) return rustStepItems(value, steps.reverse(), context);
|
||||
const call = rustTrailingCall(v);
|
||||
const m = call ? /^([\s\S]+?)\s*\.\s*([a-z_]\w*)$/.exec(call.callee) : null;
|
||||
if (!m || !(RUST_ITEM_ADAPTERS.has(m[2]!) || RUST_ITEM_PROJECTIONS.has(m[2]!))) return null;
|
||||
steps.push(m[2]!);
|
||||
v = m[1]!;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The items a value hands out through std adapters, innermost first. */
|
||||
function rustStepItems(value: RustWrittenType, steps: string[], context: ResolutionContext): RustWrittenType | null {
|
||||
let projection: 'key' | 'value' | undefined;
|
||||
let numbered = false;
|
||||
for (const step of steps) {
|
||||
const p = RUST_ITEM_PROJECTIONS.get(step);
|
||||
if (p === 'enumerate') {
|
||||
if (numbered) return null;
|
||||
numbered = true;
|
||||
} else if (p) {
|
||||
if (numbered || projection) return null;
|
||||
projection = p;
|
||||
}
|
||||
}
|
||||
const item = rustItemOf(value, projection, context);
|
||||
return item && numbered ? { ...item, text: `(usize, ${item.text})` } : item;
|
||||
}
|
||||
|
||||
/** Std collections whose items are their one type argument. */
|
||||
const RUST_STD_SEQUENCES: ReadonlySet<string> = new Set([
|
||||
'Vec', 'VecDeque', 'HashSet', 'BTreeSet', 'BinaryHeap', 'LinkedList', 'Option', 'IndexSet', 'SmallVec',
|
||||
]);
|
||||
const RUST_STD_MAPS: ReadonlySet<string> = new Set(['HashMap', 'BTreeMap', 'IndexMap']);
|
||||
/** Std iterator types by what they hand out: an item, a map's entry, key or value. */
|
||||
const RUST_STD_ITERATORS: ReadonlyMap<string, 'item' | 'entry' | 'key' | 'value'> = new Map([
|
||||
['Iter', 'entry'], ['IterMut', 'entry'], ['IntoIter', 'entry'], ['Drain', 'entry'],
|
||||
['Keys', 'key'], ['IntoKeys', 'key'], ['Values', 'value'], ['ValuesMut', 'value'], ['IntoValues', 'value'],
|
||||
]);
|
||||
const RUST_ITERATOR_TRAITS: ReadonlySet<string> = new Set(['Iterator', 'IntoIterator', 'DoubleEndedIterator', 'ExactSizeIterator']);
|
||||
|
||||
/**
|
||||
* The item type a value of a Rust type hands out when iterated: a std
|
||||
* sequence's or a slice's element, a std map's `(K, V)` (its key or value
|
||||
* through `.keys()` / `.values()`), a std iterator's item, an `impl Iterator<Item
|
||||
* = T>`'s `T`, through `&` and the pointers that deref to what they hold.
|
||||
* Read where the type is written. Null for any other type.
|
||||
*/
|
||||
function rustItemOf(written: RustWrittenType, projection: 'key' | 'value' | undefined, context: ResolutionContext): RustWrittenType | null {
|
||||
let t = written.text.trim();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const before = t;
|
||||
t = t.replace(/^&\s*(?:'\w+\s+)?(?:mut\s+)?/, '').trim();
|
||||
const pointer = /^(?:(?:std|alloc|core)\s*::\s*(?:\w+\s*::\s*)*)?(?:Box|Rc|Arc|Cow|Ref|RefMut|MutexGuard|RwLockReadGuard|RwLockWriteGuard)\s*</.exec(t);
|
||||
if (pointer && rustAngleEnd(t, pointer[0].length - 1) === t.length - 1) t = t.slice(pointer[0].length, -1).replace(/^\s*'\w+\s*,/, '').trim();
|
||||
if (t === before) break;
|
||||
}
|
||||
const as = (text: string | undefined): RustWrittenType | null => (text ? { ...written, text: text.trim(), wrappers: undefined } : null);
|
||||
const typeArgs = (args: string): string[] => splitRustTopLevel(args, ',').map((a) => a.trim()).filter((a) => a !== '' && !a.startsWith("'"));
|
||||
const std = (name: string): boolean => !isRustProjectNamed(name, RUST_DECLARED_TYPE_KINDS, context);
|
||||
if (t.startsWith('[')) {
|
||||
if (!rustGroupIsWhole(t) || projection) return null;
|
||||
return as(splitRustTopLevel(t.slice(1, -1), ';')[0]);
|
||||
}
|
||||
const object = /^(?:impl|dyn)\s+([\s\S]+)$/.exec(t);
|
||||
if (object) {
|
||||
if (projection) return null;
|
||||
for (const bound of splitRustTopLevel(object[1]!, '+')) {
|
||||
const head = rustPathHead(bound.trim());
|
||||
if (!head || !RUST_ITERATOR_TRAITS.has(head.name)) continue;
|
||||
return as(typeArgs(head.args).map((a) => /^Item\s*=\s*([\s\S]+)$/.exec(a)?.[1]).find((a) => a !== undefined));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const head = rustPathHead(t);
|
||||
if (!head || !std(head.name)) return null;
|
||||
const args = typeArgs(head.args);
|
||||
if (RUST_STD_SEQUENCES.has(head.name)) return projection || args.length !== 1 ? null : as(args[0]);
|
||||
if (RUST_STD_MAPS.has(head.name)) {
|
||||
if (args.length < 2) return null;
|
||||
return projection === 'key' ? as(args[0]) : projection === 'value' ? as(args[1]) : as(`(${args[0]}, ${args[1]})`);
|
||||
}
|
||||
const iterator = RUST_STD_ITERATORS.get(head.name);
|
||||
if (!iterator) return null;
|
||||
if (iterator === 'key' || iterator === 'value') return projection || args.length !== 2 ? null : as(args[iterator === 'key' ? 0 : 1]);
|
||||
if (args.length === 1) return projection ? null : as(args[0]);
|
||||
if (args.length === 2) return projection === 'key' ? as(args[0]) : projection === 'value' ? as(args[1]) : as(`(${args[0]}, ${args[1]})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -10128,6 +10301,12 @@ function rustFutureOutput(written: RustWrittenType): RustWrittenType | null {
|
||||
* type its declaration writes.
|
||||
*/
|
||||
function rustSiteType(site: RustPatternSite, scrutinee: string | undefined, at: number, scopes: RustScopes, ref: UnresolvedRef, context: ResolutionContext, depth: number): RustWrittenType | null {
|
||||
// `let (a, b) = …`, `(k, v) =>`: the matched tuple's field at the name's place.
|
||||
if (site.path === '') {
|
||||
if (site.index === undefined || !scrutinee) return null;
|
||||
const value = rustValueType(scrutinee, at, scopes, ref, context, depth + 1);
|
||||
return value ? rustTupleField(value, site.index) : null;
|
||||
}
|
||||
const segments = site.path.split('::');
|
||||
const last = segments[segments.length - 1]!;
|
||||
if (segments.length === 1 && (last === 'Some' || last === 'Ok' || last === 'Err')) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
/** 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`. */
|
||||
/** The pattern's path as written: `Commands::Build`, `Self::Text`, `Some`; '' for a tuple's own `(a, b)`. */
|
||||
path: string;
|
||||
/** The name's position among a tuple pattern's fields. */
|
||||
index?: number;
|
||||
@@ -36,7 +36,7 @@ export interface RustBinding {
|
||||
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. */
|
||||
/** What an `if let`, `while let`, `let … else` or `match` pattern is matched against, or what a `for` iterates. */
|
||||
scrutinee?: string;
|
||||
}
|
||||
|
||||
@@ -513,8 +513,23 @@ function readRustBindings(scopes: RustScopes): Map<string, RustBinding[]> {
|
||||
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 }));
|
||||
const emit: Emit = (name, at, site) => add(name, { at, kind, from: scopeFrom, to: scopeTo, site, scrutinee });
|
||||
// A tuple pattern, `(k, v)` or `(i, mut x)`: a name alone at its top is
|
||||
// the tuple's field at its place (a site with no path).
|
||||
const open = skipSpace(code, from);
|
||||
const end = code[open] === '(' ? close.get(open) : undefined;
|
||||
if (end !== undefined && end < to && skipSpace(code, end + 1) >= to) {
|
||||
let index = 0;
|
||||
for (const [a, b] of splitTop(code, open + 1, end, close, false)) {
|
||||
const text = code.slice(a, b);
|
||||
const simple = /^\s*(?:ref\s+)?(?:mut\s+)?([A-Za-z_]\w*)\s*$/.exec(text);
|
||||
if (simple && isBindingName(simple[1]!)) emit(simple[1]!, a + text.lastIndexOf(simple[1]!), { path: '', index });
|
||||
else walkPattern(code, a, b, close, undefined, emit);
|
||||
index++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
walkPattern(code, from, to, close, undefined, emit);
|
||||
};
|
||||
// A parameter `pattern: Type`, or a closure's `pattern`.
|
||||
const param = (from: number, to: number, kind: 'param' | 'closure', scopeFrom: number, scopeTo: number): void => {
|
||||
@@ -650,7 +665,8 @@ function readRustBindings(scopes: RustScopes): Map<string, RustBinding[]> {
|
||||
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);
|
||||
// What the loop iterates is its bindings' scrutinee.
|
||||
bindPattern(p, inAt, 'for', open, close.get(open) ?? n, code.slice(inAt + 2, open).trim());
|
||||
}
|
||||
|
||||
for (const m of code.matchAll(/\bmatch\b/g)) {
|
||||
|
||||
Reference in New Issue
Block a user