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:
Colby Mchenry
2026-10-20 20:33:36 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e043efbc84
commit 4286083a84
4 changed files with 421 additions and 10 deletions
+1
View File
@@ -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']);
});
});
+184 -5
View File
@@ -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')) {
+21 -5
View File
@@ -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)) {