fix(rust): a bare name is in scope only through its module or a use (#2214)

The Rust scope rule (#2124) only checked prelude names; every other bare
name could mean any same-named project item, and a name after any path
(`io::Result`) could mean any at all. The Rust framework resolver's
PascalCase / handler / service patterns bound names with no scope check at
0.7-0.8. tokio's 1,221 `io::Result<...>` went to a private
`runtime::task::Result` alias, 642 `Context<'_>` (from `use
std::task::{Context, Poll}`) to `runtime::context::Context`.

- A type or function from another file is in scope only through a `use`
  that binds it (its leaf, `as` alias, or `{self}`) or a glob over its
  module; methods and fields (reached through a value), import refs and
  module / file targets are unaffected.
- A name a file imports from outside the project - `std` / `core` /
  `alloc`, or a crate its Cargo manifests declare as a dependency - is that
  outside item. (An inline `mod support { pub mod panic; }` is the
  project's.)
- A path names its module: `seg::Name` reaches an item of module `seg`;
  a path rooted at std (`std::io::Error`) or at an outside import (`io::`
  under `use std::io`) reaches none; `crate::` / `self::` / `super::` and a
  project crate's name (`clap::Command`, re-exported) look it up as before.
  The path is read anywhere on the line, not only at the reference's column.
- The framework resolver's patterns apply the same check.

tokio -3008 +610, serde -792 +650, ripgrep -602 +247, axum -453 +186, clap
-342 +32, bat -38 +9. Sampled losses: std / core / futures / syn / http /
anyhow imports and paths, associated types (`S::Ok`, `S::Error`, `V::Value`,
`Fut::Output`); gains are same-module items the old first match hid.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-30 22:54:08 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 5f1a635714
commit fa8d7284c5
4 changed files with 217 additions and 7 deletions
+14 -3
View File
@@ -8,6 +8,17 @@ import { Node } from '../../types';
import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
import { stripCommentsForRegex } from '../strip-comments';
import { getCargoWorkspaceCrateMap } from './cargo-workspace';
import { isRustNameInScope } from '../name-matcher';
/**
* Whether the item a name heuristic found is one the reference can name:
* `Context<'_>` under `use std::task::{Context, Poll}` is std's, not tokio's
* `runtime::context::Context` (642 of them).
*/
function inRustScope(id: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
const node = context.getNodeById?.(id);
return !node || isRustNameInScope(node, ref, context);
}
const cargoWorkspaceMapCache = new WeakMap<ResolutionContext, Map<string, string>>();
@@ -32,7 +43,7 @@ export const rustResolver: FrameworkResolver = {
// Pattern 1: Handler references
if (ref.referenceName.endsWith('_handler') || ref.referenceName.startsWith('handle_')) {
const result = resolveByNameAndKind(ref.referenceName, FUNCTION_KINDS, HANDLER_DIRS, context);
if (result) {
if (result && inRustScope(result, ref, context)) {
return {
original: ref,
targetNodeId: result,
@@ -45,7 +56,7 @@ export const rustResolver: FrameworkResolver = {
// Pattern 2: Service/Repository trait implementations
if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Repository')) {
const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
if (result) {
if (result && inRustScope(result, ref, context)) {
return {
original: ref,
targetNodeId: result,
@@ -58,7 +69,7 @@ export const rustResolver: FrameworkResolver = {
// Pattern 3: Struct references (PascalCase)
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
const result = resolveByNameAndKind(ref.referenceName, STRUCT_KINDS, MODEL_DIRS, context);
if (result) {
if (result && inRustScope(result, ref, context)) {
return {
original: ref,
targetNodeId: result,