From 4286083a844c4105c70d168c1ddec8259cf18dec Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Tue, 20 Oct 2026 20:33:36 +0000 Subject: [PATCH] fix(rust): a for-loop variable or a tuple pattern's name has the type it is bound from (#2717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`'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 --- CHANGELOG.md | 1 + __tests__/rust-loop-tuple-field-types.test.ts | 215 ++++++++++++++++++ src/resolution/name-matcher.ts | 189 ++++++++++++++- src/resolution/rust-scopes.ts | 26 ++- 4 files changed, 421 insertions(+), 10 deletions(-) create mode 100644 __tests__/rust-loop-tuple-field-types.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b622ddb..7152433d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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::()`, and a call on a type that implements one trait twice, like `Encoder` 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`, `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. diff --git a/__tests__/rust-loop-tuple-field-types.test.ts b/__tests__/rust-loop-tuple-field-types.test.ts new file mode 100644 index 00000000..13b3ecc5 --- /dev/null +++ b/__tests__/rust-loop-tuple-field-types.test.ts @@ -0,0 +1,215 @@ +/** + * A Rust `for` loop's variable is an item of what the loop iterates — a + * field's `Vec`, a `HashMap`'s `(K, V)`, `.enumerate()`'s + * `(usize, T)`, a method's `impl Iterator` — 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 = { + '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,', + ' pub index: std::collections::HashMap,', + ' pub pair: (Key, Item),', + '}', + '', + 'impl Store {', + ' pub fn all(&self) -> impl Iterator {', + ' 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']); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index ea3d1b14..e8780e1d 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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 = 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 = 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` an `Arg`, `for x + * in &self.items` with `items: Vec` 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 = new Set([ + 'Vec', 'VecDeque', 'HashSet', 'BTreeSet', 'BinaryHeap', 'LinkedList', 'Option', 'IndexSet', 'SmallVec', +]); +const RUST_STD_MAPS: ReadonlySet = 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 = 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 = 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`'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* (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')) { diff --git a/src/resolution/rust-scopes.ts b/src/resolution/rust-scopes.ts index 4b896806..5e8215f3 100644 --- a/src/resolution/rust-scopes.ts +++ b/src/resolution/rust-scopes.ts @@ -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 { 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 { 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)) {