From 60b2f1e7bbaada4cc3c0d3ff275fb52e2128516e Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Tue, 20 Oct 2026 01:37:17 +0000 Subject: [PATCH] fix(rust): a call reaches a project impl on a std type only as Rust would pick it (#2697) A crate can't declare a method on String, Vec, Arc or NonNull, only implement a trait for one, and name matching took those impls for any call of that name. Every String::from("...") in dioxus's CLI went to its impl From for String, tokio's NonNull::from(waiter) calls to its impl From<&Handle> for NonNull, its tests' files.iter() to an internal Storage trait's impl for Vec, and clap's String::from calls to its impl From for OsString. A call into a method of a project impl on a std type now stands only when Rust could pick it. A std trait (From, PartialEq, ...) is implemented for the crate's own type argument, so the call must pass one: built from it (IDI::Install), self in an impl of it, or a name the enclosing function declares with it (fn from(name: Str)). A trait of the project's must be in scope in the calling file: declared, implemented or imported there, or brought in by a glob that may carry it. Otherwise the call is left unresolved, since what it calls is the standard library's. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/rust-foreign-impl-calls.test.ts | 140 +++++++++++++++++++ src/resolution/index.ts | 13 +- src/resolution/name-matcher.ts | 162 ++++++++++++++++++++++ 4 files changed, 312 insertions(+), 4 deletions(-) create mode 100644 __tests__/rust-foreign-impl-calls.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 33efde86b..053c09151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -210,6 +210,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In JavaScript and TypeScript, a barrel's `export { A, B as C } from './x'` now links each name it passes on to what `./x` exports, following barrels that pass it on in turn. Before, each name was looked up by itself and could link to a same-named symbol elsewhere in the project, such as another folder's `Provider`, and a name passed on from a package, like `export { debounce } from 'lodash'`, could link to a project function of that name. Re-index JavaScript and TypeScript projects after upgrading. - In Swift, a call written after `!` or another prefix operator, or chained after `try` or `await`, now links to what it calls. `if !isReady()` links to `isReady`, `!store.contains(id)` and `try! JSONDecoder().decode(…)` link to the method of the type they're called on rather than to whichever `contains` or `decode` came first, and `!Settings.Defaults.isOn()` lands on the type it names. Re-index Swift projects after upgrading. - In Swift, a call on a local created through `try`, `try!`, `try?` or `await`, or with a generic type's constructor, now links to that type's method: after `let storage = try! DiskStorage.Backend(config: config)`, `storage.store(…)` links to `Backend`'s `store`. Before, such a local had no known type, so the call was left unlinked or matched by name to another type's method. Re-index Swift projects after upgrading. +- In Rust, a call like `String::from("…")`, `Vec::from([…])` or `NonNull::from(x)` no longer links to your project's own `impl From for String` (or for `Vec`, `NonNull`, …) unless what it passes is a `YourType`. A method call on a standard-library value, like `files.iter()` or `buf.extend(…)`, links to your trait's implementation for that type only where the trait is imported. Before, every `String::from("…")` call in dioxus's CLI linked to one such conversion, and tokio's `vec.iter()` calls to an internal trait's `Vec` impl. Re-index Rust projects after upgrading. - In Rust, a type alias, struct, enum or function declared without `pub` now links only from its own module and the modules inside it, and a name a `use` brings in from one of your workspace's crates, like `use tokio_test::task::spawn` or `use crate::error::Error`, links only to an item of that crate or of a crate it depends on. Before, a file's `use crate::Result` could link its `Result<…>` types to another module's private `type Result`, as in dioxus's CLI, and a call to an imported function could link to another crate's function of the same name, such as tokio's test `spawn(…)` calls to one in its examples, or to nothing because a private function of that name was in the way. Re-index Rust projects after upgrading. ## [1.6.2] - 2026-10-03 diff --git a/__tests__/rust-foreign-impl-calls.test.ts b/__tests__/rust-foreign-impl-calls.test.ts new file mode 100644 index 000000000..5d9256299 --- /dev/null +++ b/__tests__/rust-foreign-impl-calls.test.ts @@ -0,0 +1,140 @@ +/** + * A crate can't declare a method on `String`, `Vec`, `Arc` or `NonNull`, only + * implement a trait for one. A call reaches such an impl only as Rust picks it: + * - a std trait the crate implements for one of its own types (`impl + * From for String`) is what `String::from(x)` calls only when `x` is an + * `IDI`. dioxus's CLI's `String::from("…")` calls all went to that impl, + * tokio's `NonNull::from(waiter)` to its `impl From<&Handle> for NonNull`; + * - a trait of the crate's own (`impl Storage for Vec`) is reached only + * where it is in scope: tokio's tests' `files.iter()` on a `Vec` went to its + * `Storage` impl, in files that never import `Storage`. + */ +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-foreign-impl-')); + const files: Record = { + 'Cargo.toml': '[package]\nname = "cli"\nversion = "0.1.0"\n', + 'src/main.rs': 'mod cli;\nmod names;\nmod slots;\nmod store;\nmod windows;\n\nfn main() {}\n', + 'src/windows.rs': [ + 'pub enum Idi {', + ' Install,', + '}', + '', + 'impl From for String {', + ' fn from(_e: Idi) -> Self {', + ' String::new()', + ' }', + '}', + '', + 'pub fn label() -> String {', + ' String::from(Idi::Install)', + '}', + '', + ].join('\n'), + 'src/names.rs': [ + 'use std::ffi::OsString;', + '', + 'pub struct Str;', + '', + 'impl From for OsString {', + ' fn from(_name: Str) -> Self {', + ' OsString::new()', + ' }', + '}', + '', + 'pub fn os_name(name: Str) -> OsString {', + ' OsString::from(name)', + '}', + '', + ].join('\n'), + 'src/cli.rs': [ + 'use std::ffi::OsString;', + '', + 'pub fn banner() -> String {', + ' String::from("dx")', + '}', + '', + 'pub fn arg() -> OsString {', + ' OsString::from("--release")', + '}', + '', + ].join('\n'), + 'src/store.rs': [ + 'pub trait Storage {', + ' fn iter(&self) -> usize;', + '}', + '', + 'pub struct Slot;', + '', + 'impl Storage for Vec {', + ' fn iter(&self) -> usize {', + ' self.len()', + ' }', + '}', + '', + ].join('\n'), + // Storage in scope: its `iter`. + 'src/slots.rs': [ + 'use crate::store::{Slot, Storage};', + '', + 'pub fn count(slots: Vec) -> usize {', + ' slots.iter()', + '}', + '', + ].join('\n'), + // No Storage here: a Vec's own `iter`. + 'tests/files.rs': [ + 'pub fn paths(files: Vec) -> usize {', + ' files.iter().count()', + '}', + '', + ].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 `name`, called in `file`, links to, as `file@line` of each target, sorted. */ +const callsOf = (file: string, name: string): string[] => { + const out: string[] = []; + for (const node of cg.getNodesInFile(file)) { + for (const edge of cg.getOutgoingEdges(node.id)) { + if (edge.kind !== 'calls') continue; + const target = cg.getNode(edge.target)!; + if (target.name === name) out.push(`${target.filePath}@${target.startLine}`); + } + } + return [...new Set(out)].sort(); +}; + +describe('a Rust call into a project impl on a std type', () => { + it('is not a std trait impl unless its argument is the impl\'s own type', () => { + expect(callsOf('src/cli.rs', 'from')).toEqual([]); + }); + + it('reaches it when the argument is that type: built from it, or declared with it', () => { + expect(callsOf('src/windows.rs', 'from')).toEqual(['src/windows.rs@6']); + expect(callsOf('src/names.rs', 'from')).toEqual(['src/names.rs@6']); + }); + + it('reaches a project trait\'s impl only where the trait is in scope', () => { + expect(callsOf('src/slots.rs', 'iter')).toEqual(['src/store.rs@8']); + expect(callsOf('tests/files.rs', 'iter')).toEqual([]); + }); +}); diff --git a/src/resolution/index.ts b/src/resolution/index.ts index a6aeb6f2f..da621ada0 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -21,7 +21,7 @@ import { isImportableKind, CPP_DEFINE_SIGNATURE, } from './types'; -import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, GO_TYPE_KINDS, matchGoAssertedCall, isGoAssertedLocal, isGoLocallyBound } from './name-matcher'; +import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, GO_TYPE_KINDS, matchGoAssertedCall, isGoAssertedLocal, isGoLocallyBound } from './name-matcher'; import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility'; import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor'; import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes'; @@ -3441,12 +3441,17 @@ export class ReferenceResolver { /** * A bare Rust name reaches only what is in scope — every strategy's result, * a framework resolver's `Ok(x)` → `struct Ok` construction included (see - * isRustNameInScope). + * isRustNameInScope). And a call, however written, reaches a project impl + * on a std type only as Rust would pick it (isRustForeignImplCallable). */ private gateRustScope(result: ResolvedRef | null, ref: UnresolvedRef): ResolvedRef | null { - if (!result || ref.language !== 'rust' || !/^[A-Za-z_]\w*$/.test(ref.referenceName)) return result; + if (!result || ref.language !== 'rust') return result; + const bare = /^[A-Za-z_]\w*$/.test(ref.referenceName); + if (!bare && ref.referenceKind !== 'calls') return result; const target = this.nodeById(result.targetNodeId); - return target && !isRustNameInScope(target, ref, this.context) ? null : result; + if (!target) return result; + if (bare && !isRustNameInScope(target, ref, this.context)) return null; + return ref.referenceKind === 'calls' && !isRustForeignImplCallable(target, ref, this.context) ? null : result; } /** Is `filePath` (project-relative) one of the files the index holds? */ diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 3534074b9..88f0fee1d 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -7876,6 +7876,167 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context: return uses.names.has(name) || rustGlobCovers(uses, candidate, ref); } +/** + * Std types a crate can implement a trait for but never declare a method on: + * a method the index holds for one is the project's `impl SomeTrait for + * String` (`Vec`, `Arc`, `NonNull`, …). + */ +const RUST_STD_TYPES = new Set([ + 'String', 'str', 'Vec', 'VecDeque', 'HashMap', 'HashSet', 'BTreeMap', 'BTreeSet', 'BinaryHeap', 'LinkedList', + 'Box', 'Rc', 'Arc', 'Weak', 'Cow', 'Cell', 'RefCell', 'Mutex', 'RwLock', 'Option', 'Result', 'PathBuf', 'Path', + 'OsString', 'OsStr', 'CString', 'CStr', 'NonNull', 'Pin', 'Duration', 'Instant', 'SystemTime', 'PhantomData', + 'u8', 'u16', 'u32', 'u64', 'u128', 'usize', 'i8', 'i16', 'i32', 'i64', 'i128', 'isize', 'f32', 'f64', 'bool', 'char', +]); +const RUST_DECLARED_TYPE_KINDS = new Set(['struct', 'enum', 'union', 'type_alias', 'trait']); + +interface RustImplHeader { + /** The trait's name, the last segment of its path (`linked_list::Link` → `Link`). */ + trait: string; + /** The type names its arguments write (`From<&'a Handle>` → `Handle`). */ + traitArgs: string[]; + /** The implementing type's name (`NonNull` → `NonNull`). */ + self: string; +} + +const RUST_IMPL_HEADERS = new WeakMap>(); + +/** `impl<'a, T> From<&'a Handle> for NonNull where … {` → its trait and type; null for an inherent impl. */ +function parseRustImplHeader(text: string): RustImplHeader | null { + let rest = text.replace(/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?impl\b/, '').replace(/\{[\s\S]*$/, '').replace(/\bwhere\b[\s\S]*$/, '').trim(); + // The impl's own generics, `<'a, T: Into>`. + if (rest.startsWith('<')) { + let depth = 0; + let i = 0; + for (; i < rest.length; i++) { + if (rest[i] === '<') depth++; + else if (rest[i] === '>' && --depth === 0) break; + } + rest = rest.slice(i + 1).trim(); + } + // ` for ` outside the angle brackets parts the trait from the type. + let depth = 0; + let split = -1; + for (let i = 0; i < rest.length && split < 0; i++) { + if (rest[i] === '<') depth++; + else if (rest[i] === '>') depth--; + else if (depth === 0 && /\s/.test(rest[i]!) && /^for\s/.test(rest.slice(i + 1))) split = i; + } + if (split < 0) return null; + const traitPart = rest.slice(0, split).trim().replace(/^!/, ''); + const head = (part: string): string => + /([A-Za-z_]\w*)\s*(?:<|$)/.exec(part.replace(/^(?:&\s*(?:'\w+\s+)?(?:mut\s+)?|dyn\s+)+/, '').trim())?.[1] ?? ''; + const open = traitPart.indexOf('<'); + const traitArgs = open < 0 ? [] : [...traitPart.slice(open + 1).matchAll(/(? m[1]!).filter((id) => id !== 'mut' && id !== 'dyn'); + return { trait: head(open < 0 ? traitPart : traitPart.slice(0, open)), traitArgs, self: head(rest.slice(split + 5)) }; +} + +/** The `impl Trait<…> for Type<…>` a Rust method is declared in: null for an inherent impl, or none. */ +function rustTraitImplHeader(method: Node, context: ResolutionContext): RustImplHeader | null { + let memo = RUST_IMPL_HEADERS.get(context); + if (!memo) RUST_IMPL_HEADERS.set(context, (memo = new Map())); + const hit = memo.get(method.id); + if (hit !== undefined) return hit; + let header: RustImplHeader | null = null; + const lines = context.getFileLines?.(method.filePath) ?? context.readFile(method.filePath)?.split(/\r?\n/) ?? []; + for (let i = method.startLine - 2; i >= 0; i--) { + const line = lines[i] ?? ''; + if (/^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?impl\b/.test(line)) { + let text = line.replace(/\/\/.*$/, ''); + for (let j = i + 1; !text.includes('{') && j < Math.min(lines.length, i + 8); j++) text += ` ${(lines[j] ?? '').replace(/\/\/.*$/, '')}`; + header = parseRustImplHeader(text); + break; + } + // A top-level item above the method means it was not inside an impl. + if (/^(?:pub(?:\([^)]*\))?\s+)?(?:fn|struct|enum|mod|trait|const|static|type)\b/.test(line)) break; + } + memo.set(method.id, header); + return header; +} + +function isRustProjectNamed(name: string, kinds: ReadonlySet, context: ResolutionContext): boolean { + return context.getNodesByName(name).some((n) => n.language === 'rust' && kinds.has(n.kind)); +} + +const RUST_TRAIT_KIND = new Set(['trait']); + +/** + * Whether a call's first argument is evidently a `type`: a value built through + * it (`IDI::Install`, `Str::from(…)`), `self` in an impl of it, or a name the + * enclosing function declares with it (`fn from(name: Str)`, `let s: Str`). + */ +function rustFirstArgIs(type: string, ref: UnresolvedRef, context: ResolutionContext): boolean { + const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? []; + const method = ref.referenceName.slice(Math.max(ref.referenceName.lastIndexOf(':'), ref.referenceName.lastIndexOf('.')) + 1); + const text = [(lines[ref.line - 1] ?? '').slice(Math.max(0, ref.column)), ...lines.slice(ref.line, ref.line + 4)].join(' '); + const open = new RegExp(`\\b${method}\\s*\\(`).exec(text); + // Not where the reference says: nothing to read, so nothing to veto. + if (!open) return true; + let depth = 0; + let end = open.index + open[0].length; + for (; end < text.length; end++) { + const ch = text[end]!; + if ('([{'.includes(ch)) depth++; + else if (')]}'.includes(ch)) { + if (depth === 0) break; + depth--; + } else if (ch === ',' && depth === 0) break; + } + const arg = text.slice(open.index + open[0].length, end).trim().replace(/^(?:&\s*(?:mut\s+)?|\*)+/, '').replace(/\.clone\(\)$/, ''); + if (new RegExp(`^(?:[A-Za-z_]\\w*::)*${type}\\b`).test(arg)) return true; + if (!/^[A-Za-z_]\w*$/.test(arg)) return false; + const declared = new RegExp(`\\b${arg}\\s*:\\s*&?\\s*(?:'\\w+\\s+)?(?:mut\\s+)?(?:[A-Za-z_]\\w*::)*${type}\\b`); + const built = new RegExp(`\\blet\\s+(?:mut\\s+)?${arg}\\s*=\\s*&?\\s*(?:[A-Za-z_]\\w*::)*${type}\\b`); + for (let i = ref.line - 1; i >= 0; i--) { + const line = lines[i] ?? ''; + if (arg === 'self' && /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?impl\b/.test(line)) { + const impl = parseRustImplHeader(`${line} {`); + return (impl?.self ?? /impl\b(?:\s*<[^>]*>)?\s*(?:[A-Za-z_]\w*::)*([A-Za-z_]\w*)/.exec(line)?.[1]) === type; + } + if (arg !== 'self' && (declared.test(line) || built.test(line))) return true; + // Above the enclosing function, nothing more declares it. + if (arg !== 'self' && /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:const\s+)?(?:async\s+)?(?:unsafe\s+)?(?:extern\s+"[^"]*"\s+)?fn\s/.test(line)) break; + } + return false; +} + +/** Whether the file of `ref` has the project trait `trait` in scope: declared or implemented there, or brought in by a `use`. */ +function isRustTraitInScope(trait: string, ref: UnresolvedRef, target: Node, context: ResolutionContext): boolean { + if (ref.filePath === target.filePath) return true; + const traits = context.getNodesByName(trait).filter((n) => n.kind === 'trait' && n.language === 'rust'); + if (traits.some((t) => t.filePath === ref.filePath)) return true; + const uses = rustUsesOf(ref.filePath, context); + if (uses.names.has(trait)) return true; + // A glob over a prelude or a crate's root may hand it on: it can't be told apart. + const crates = rustProjectCrates(context); + if ([...uses.globs].some((g) => g === 'prelude' || g === 'crate' || crates.has(g))) return true; + return traits.some((t) => rustGlobCovers(uses, t, ref)); +} + +/** + * Whether a Rust call can reach this method of a project `impl` on a std type, + * as Rust picks one. A crate implements only a trait for `String`, `Vec`, + * `Arc` or `NonNull`, never a method of its own: + * - a std trait, which it can implement only for one of its own types + * (`impl From for String`): `String::from(x)` is that impl's only when + * `x` is an `IDI`. dioxus's `String::from("…")` calls all went to it, + * tokio's `NonNull::from(waiter)` to its `impl From<&Handle> for NonNull`; + * - a trait of the project's (`impl Link for Arc`), reached only + * where the trait is in scope: tokio's `Arc::from_raw(ptr)` in `util/wake.rs` + * is std's own, its tests' `files.iter()` a `Vec`'s, not a `Storage` impl's. + */ +export function isRustForeignImplCallable(target: Node, ref: UnresolvedRef, context: ResolutionContext): boolean { + if (target.language !== 'rust' || target.kind !== 'method') return true; + const cut = target.qualifiedName.lastIndexOf('::'); + const owner = cut < 0 ? '' : target.qualifiedName.slice(0, cut).split('::').pop()!; + if (!RUST_STD_TYPES.has(owner) || isRustProjectNamed(owner, RUST_DECLARED_TYPE_KINDS, context)) return true; + const impl = rustTraitImplHeader(target, context); + if (!impl || impl.self !== owner || impl.trait === '') return true; + if (isRustProjectNamed(impl.trait, RUST_TRAIT_KIND, context)) return isRustTraitInScope(impl.trait, ref, target, context); + const local = impl.traitArgs.find((t) => isRustProjectNamed(t, RUST_DECLARED_TYPE_KINDS, context)); + return local === undefined || rustFirstArgIs(local, ref, context); +} + /** Whether `type` is a Rust enum that declares `variant` (#2328). */ function declaresRustVariant(type: Node, variant: string, context: ResolutionContext): boolean { if (type.kind !== 'enum') return false; @@ -13394,6 +13555,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { RUST_CRATES.delete(context); RUST_DEPENDENCIES.delete(context); RUST_CRATE_GRAPHS.delete(context); + RUST_IMPL_HEADERS.delete(context); LEXICAL_SCOPE_MEMO.delete(context); KOTLIN_LAMBDA_RECEIVERS.delete(context); SCALA_IMPORTED_SUPERS.delete(context);