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<IDI> for String, tokio's NonNull::from(waiter) calls to its
impl From<&Handle> for NonNull<Entry>, its tests' files.iter() to an
internal Storage trait's impl for Vec, and clap's String::from calls
to its impl From<Str> 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 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-20 01:37:17 +00:00
committed by GitHub
parent 3273f20f47
commit 60b2f1e7bb
4 changed files with 312 additions and 4 deletions
+1
View File
@@ -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<String>(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<YourType> 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
+140
View File
@@ -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<IDI> 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<Entry>`;
* - a trait of the crate's own (`impl Storage for Vec<Slot>`) 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<string, string> = {
'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<Idi> 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<Str> 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<Slot> {',
' 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<Slot>) -> usize {',
' slots.iter()',
'}',
'',
].join('\n'),
// No Storage here: a Vec's own `iter`.
'tests/files.rs': [
'pub fn paths(files: Vec<String>) -> 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([]);
});
});
+9 -4
View File
@@ -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? */
+162
View File
@@ -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<u8>`, `Arc<T>`, `NonNull<T>`, …).
*/
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<Entry>` → `NonNull`). */
self: string;
}
const RUST_IMPL_HEADERS = new WeakMap<ResolutionContext, Map<string, RustImplHeader | null>>();
/** `impl<'a, T> From<&'a Handle> for NonNull<Entry> 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<U>>`.
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(/(?<!['\w])([A-Za-z_]\w*)/g)]
.map((m) => 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<string>, 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<IDI> 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<Entry>`;
* - a trait of the project's (`impl Link for Arc<ScheduledIo>`), 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);