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
co-authored by Claude Opus 5.5
parent 3273f20f47
commit 60b2f1e7bb
4 changed files with 312 additions and 4 deletions
+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);