mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-29 06:46:49 +08:00
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:
co-authored by
Claude Opus 5.5
parent
3273f20f47
commit
60b2f1e7bb
@@ -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? */
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user