fix(resolution): a bare Rust name reaches only what is in scope (#2124)

Found by the README-wide sweep: ripgrep bound 469 `Some(x)` calls to its
own `EncodingMode::Some` variant and 536 `Ok(x)` to `ParseResult::Ok`;
serde bound its `Ok(…)`/`Result<…>` to the `struct Ok` / `struct Result`
its macro-hygiene test declares (845 on one); tokio 924 `Ok` onto
`TransitionToIdle::Ok`, 290 `drop(x)` onto a `drop` method.

For a name that is bare IN THE SOURCE (a `::` path is kept by its last
segment and is not a prelude lookup — checked at the reference's column):
- an enum variant is in scope only through a `use` of it (and its enum) or
  of its enum's `*`, and never names a type;
- a prelude name (Ok/Err/Some/None/Result/Option/Box/Vec/String/drop/…)
  reaches a project item in another file only through a project `use` of
  that name or a glob of that item's own module (`use crate::glob::*`,
  `use super::*` for the parent) — `use std::…` trees don't count, and the
  `use` scan runs on comment-stripped text (a doc comment's "…use the
  Option…" once read as an import);
- fuzzy matching is case-exact for Rust (`Bytes` ≠ method `bytes`,
  `Ok` ≠ fn `ok`).
Applied as a candidate filter (so the in-scope item can win) and at
resolveOne's seam — a Rust framework resolver's `Ok(x)` → `struct Ok`
construction bypassed name matching.

A/B vs main (edge diffs, every change classified): ripgrep −1,462/+0,
serde −2,072/+40, tokio −1,820/+2, clap −752/+7, axum −397/+4, bat −45/+0 —
losses are Ok/Some/Err/Result/Option/Vec/drop and same-named variants
pinned on project items; gains are pre-existing same-name ambiguity
(serde's two `Content` enums).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-30 05:34:51 +00:00
committed by GitHub
parent c5ba68076a
commit ddc03f7c79
4 changed files with 182 additions and 5 deletions
+1
View File
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
- In Rust, a bare `Ok(…)`, `Some(…)`, `Err(…)`, `Result<…>`, `Option<…>`, `Vec<…>` or `drop(…)` now means the standard library's, unless the file defines or imports a project item of that name. These used to link to any same-named enum variant or struct in the project: every `Some(x)` in ripgrep to one enum's `Some` variant, and every `Ok` and `Result` in serde to structs its macro-hygiene tests declare. An enum variant is now reached by a bare name only where the file `use`s it or its enum's `*`, and a variant never stands for a type. Re-index Rust projects after upgrading.
- A generic type parameter, like `A` in `def zipWith[A, B](…)`, `T` in `<T> T max(…)` or `class Foo<T>`, no longer links to a project symbol that happens to share its name. On cats, one `implicit def A` had thousands of made-up dependents from every `A` in the library. The same goes for Rust, Dart, TypeScript, Java, Kotlin, Swift, Go, C# and C++. A class or other type declared inside a function is only linked from inside that function. In Scala, a `def`'s own parameter, like `f` in `(f: A => B)`, no longer links to a same-named field elsewhere. A bare name no longer reaches a type nested inside another type unless the code is inside that type, extends it or imports its members; the type in scope is linked instead, like cats' own `FlatMap` in place of `Eval`'s nested `FlatMap`. Re-index projects in these languages after upgrading.
- A minified or bundled JavaScript file is now recognized as generated even when it isn't named `.min.js`, for example a docs site's `bundle.js`, a readable webpack build or a vendored `underscore-min.js`. Its one-letter functions now rank below your own code in search and `codegraph_explore` answers.
- In C# and VB.NET, a type written in code, like `Type sourceType`, `List<int>`, `new TypeMap()` or `Dictionary<string, Type>`, now links to the type and no longer to a property, method or field that shares its name. `List<…>` used to link to some test class's `List` property, `Type` to an attribute's `Type` property, and a parameter of type `TypeMap` to a `TypeMap` property instead of the `TypeMap` class. Those members showed up with hundreds of made-up dependents. Reading a property or passing a method, like `Builder.Services` or `MapGet("/x", GetItems)`, still links as before. Re-index C# and VB.NET projects after upgrading.
+79
View File
@@ -0,0 +1,79 @@
/**
* A bare Rust name reaches only what is in scope. `Some(x)` / `Ok(x)` are the
* prelude's: ripgrep bound every one to its own `EncodingMode::Some` and
* `ParseResult::Ok` variants, serde its `Ok`/`Result` to the structs its
* macro-hygiene test declares. A variant needs a `use` of it or its enum's `*`.
*/
import { describe, it, expect, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
const roots: string[] = [];
afterAll(() => {
for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
});
const FILES: Record<string, string> = {
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n',
'src/lowargs.rs': `pub enum EncodingMode {
Auto,
Some(u8),
Disabled,
}
pub enum ParseResult<T> {
Ok(T),
Err(String),
}
`,
'src/hygiene.rs': `struct Ok;
struct Result;
fn local() -> Ok { Ok }
`,
'src/decompress.rs': `pub fn find(cmd: u8) -> Option<u8> {
if cmd > 0 { return Some(cmd); }
let r: std::result::Result<u8, ()> = Ok(cmd);
None
}
`,
'src/error.rs': `pub struct Error;
pub type Result<T> = std::result::Result<T, Error>;
`,
'src/api.rs': `pub fn load() -> crate::error::Result<u8> { unimplemented!() }
`,
'src/modes.rs': `use crate::lowargs::EncodingMode::*;
pub fn pick() -> crate::lowargs::EncodingMode { Some(1) }
`,
};
describe('Rust: a bare name reaches what is in scope', () => {
it('the prelude, a used variant, a same-file item', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-prelude-'));
roots.push(root);
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);
}
const cg = await CodeGraph.init(root, { index: true });
try {
const targets = (fn: string): string[] => {
const from = cg.getNodesByName(fn).filter((n) => n.kind === 'function');
return cg
.getOutgoingEdgesFrom(from.map((n) => n.id), ['calls', 'references', 'instantiates'])
.map((e) => cg.getNode(e.target)!)
.map((n) => `${n.kind}:${n.qualifiedName}`);
};
// Prelude `Some` / `Ok`, not the project's variants or structs.
expect(targets('find').filter((t) => /Some|Ok|Result/.test(t))).toEqual([]);
// `use EncodingMode::*` brings the variant in.
expect(targets('pick')).toContain('enum_member:EncodingMode::Some');
// A path (`crate::error::Result`) is not a prelude lookup, so it still links.
expect(targets('load').some((t) => t.endsWith(':Result'))).toBe(true);
// A same-file struct still shadows the prelude.
expect(targets('local')).toContain('struct:Ok');
} finally {
cg.close();
}
});
});
+16 -4
View File
@@ -21,7 +21,7 @@ import {
isImportableKind,
CPP_DEFINE_SIGNATURE,
} from './types';
import { matchJsStoreBindingCall, isUnresolvedJsMemberCall, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos } from './name-matcher';
import { matchJsStoreBindingCall, isUnresolvedJsMemberCall, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope } from './name-matcher';
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility';
@@ -994,9 +994,10 @@ export class ReferenceResolver {
ref,
this.context,
);
const resolved = candidate?.resolvedBy === 'framework'
? this.gateFrameworkLanguage(candidate, ref)
: this.gateLanguage(candidate, ref);
const scoped = this.gateRustScope(candidate, ref);
const resolved = scoped?.resolvedBy === 'framework'
? this.gateFrameworkLanguage(scoped, ref)
: this.gateLanguage(scoped, ref);
if (!resolved || ref.referenceKind !== 'calls') return resolved;
const target = this.nodeById(resolved.targetNodeId);
@@ -2860,6 +2861,17 @@ export class ReferenceResolver {
return result;
}
/**
* 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).
*/
private gateRustScope(result: ResolvedRef | null, ref: UnresolvedRef): ResolvedRef | null {
if (!result || ref.language !== 'rust' || !/^[A-Za-z_]\w*$/.test(ref.referenceName)) return result;
const target = this.nodeById(result.targetNodeId);
return target && !isRustNameInScope(target, ref, this.context) ? null : result;
}
/** The repository's own package name, from its root package.json; null without one. */
private ownPackageName: string | null | undefined;
+86 -1
View File
@@ -1039,6 +1039,85 @@ function canNameInTypePosition(n: Node): boolean {
return !(n.kind === 'property' || n.kind === 'method' || n.kind === 'enum_member' || n.kind === 'field');
}
/** Names the Rust prelude puts in every module; a project item of the same name needs a `use` to shadow one. */
const RUST_PRELUDE = new Set([
'Ok', 'Err', 'Some', 'None', 'Result', 'Option', 'Box', 'Vec', 'String', 'Default', 'Drop', 'Iterator',
'IntoIterator', 'From', 'Into', 'Clone', 'Copy', 'Send', 'Sync', 'Sized', 'ToString', 'ToOwned', 'PartialEq',
'Eq', 'PartialOrd', 'Ord', 'AsRef', 'AsMut', 'Fn', 'FnMut', 'FnOnce', 'Extend', 'drop',
]);
interface RustUses {
/** Every identifier in the file's project `use` trees — not `std::` / `core::` / `alloc::` ones. */
names: Set<string>;
/** `X` of each `use …::X::*` (`super` for `use super::*`). */
globs: Set<string>;
}
const RUST_USES = new WeakMap<ResolutionContext, Map<string, RustUses>>();
function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
let memo = RUST_USES.get(context);
if (!memo) {
memo = new Map();
RUST_USES.set(context, memo);
}
const hit = memo.get(filePath);
if (hit) return hit;
const uses: RustUses = { names: new Set(), globs: new Set() };
// Comments first: a doc comment's prose ("…use the Option…") is not a `use`.
const text = stripCommentsForRegex(context.readFile(filePath) ?? '', 'rust');
for (const m of text.matchAll(/(?:^|[;{}\s])use\s+([^;]{1,2000});/g)) {
const tree = m[1]!;
if (/^\s*(?:::)?(?:std|core|alloc)\b/.test(tree)) continue;
for (const id of tree.matchAll(/[A-Za-z_]\w*/g)) uses.names.add(id[0]);
for (const g of tree.matchAll(/(\w+)\s*::\s*(?:\{[^}]*)?\*/g)) uses.globs.add(g[1]!);
}
memo.set(filePath, uses);
return uses;
}
/** The module a Rust file is: `src/glob.rs` → `glob`, `src/walk/mod.rs` → `walk`. */
function rustModuleName(filePath: string): string {
const parts = filePath.split('/');
const base = parts[parts.length - 1]!.replace(/\.rs$/, '');
return base === 'mod' || base === 'lib' || base === 'main' ? parts[parts.length - 2] ?? base : base;
}
/** Does one of the file's globs bring in this candidate's module (or, for `use super::*`, its parent's)? */
function rustGlobCovers(uses: RustUses, candidate: Node, ref: UnresolvedRef): boolean {
if (uses.globs.has(rustModuleName(candidate.filePath))) return true;
if (!uses.globs.has('super')) return false;
const dir = (p: string): string => p.slice(0, p.lastIndexOf('/'));
// `use super::*` in a child module: the parent's file, or a sibling in the parent's directory.
return dir(candidate.filePath) === dir(ref.filePath) || dir(candidate.filePath) === dir(dir(ref.filePath));
}
/**
* Whether a bare Rust name can mean this candidate. An enum's variant is in
* scope bare only through a `use` of it or of its enum's `*`, and never
* names a TYPE — ripgrep's every `Some(x)` bound to its `EncodingMode::Some`
* variant, every `Ok(x)` to `ParseResult::Ok`. A prelude name (`Ok`,
* `Result`, `Box`) is the prelude's unless the file defines it or imports a
* project item of that name — serde's macro-hygiene tests declare `struct
* Ok`, `struct Result`, and serde bound its own `Ok(…)` and `Result<…>` to them.
*/
export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
const name = ref.referenceName;
// Bare in the SOURCE: the index keeps `crate::error::Result` by its last
// segment, and a path is not a prelude lookup.
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
if (line !== undefined && line.startsWith(name, ref.column) && /::\s*$/.test(line.slice(0, ref.column))) return true;
if (candidate.kind === 'enum_member') {
if (ref.referenceKind === 'references') return false;
const uses = rustUsesOf(ref.filePath, context);
const cut = candidate.qualifiedName.lastIndexOf('::');
const owner = cut >= 0 ? candidate.qualifiedName.slice(0, cut).split('::').pop()! : '';
return (owner !== '' && uses.globs.has(owner)) || (uses.names.has(name) && uses.names.has(owner));
}
if (!RUST_PRELUDE.has(name) || candidate.filePath === ref.filePath) return true;
const uses = rustUsesOf(ref.filePath, context);
return uses.names.has(name) || rustGlobCovers(uses, candidate, ref);
}
/** What only exists inside a type, reachable through a receiver alone. */
const TYPE_MEMBER_KINDS: ReadonlySet<string> = new Set(['method', 'property', 'field', 'enum_member']);
@@ -1248,7 +1327,9 @@ export function matchByExactName(
const cMacroCall = ref.referenceKind === 'calls' && (ref.language === 'c' || ref.language === 'cpp') &&
sameName.some((n) => n.kind === 'constant' && CPP_DEFINE_SIGNATURE.test(n.signature ?? ''));
const typeRef = isDotNetTypeRef(ref, context);
const rustBare = ref.language === 'rust' && /^[A-Za-z_]\w*$/.test(ref.referenceName);
const candidates = sameName.filter((n) =>
!(rustBare && !isRustNameInScope(n, ref, context)) &&
!(cMacroCall && n.kind !== 'function' && n.kind !== 'method') &&
!(typeRef && !canNameInTypePosition(n)) &&
// A Scala type position (`Arbitrary[B]`) never names a method: an
@@ -2348,6 +2429,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
AWAITED_FILES.delete(context);
C_STATIC_MEMO.delete(context);
RUST_TRAIT_IMPL_MEMO.delete(context);
RUST_USES.delete(context);
SEALED_MODULES.delete(context);
LOCAL_BINDING_MEMO.delete(context);
LOCAL_BINDING_SITES.delete(context);
@@ -4252,7 +4334,10 @@ export function matchFuzzy(
// Filter to callable kinds only (function, method, class)
const callableKinds = new Set(['function', 'method', 'class']);
const typeRef = isDotNetTypeRef(ref, context);
const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind) && !(typeRef && !canNameInTypePosition(n)))
const rustBare = ref.language === 'rust' && /^[A-Za-z_]\w*$/.test(ref.referenceName);
// Rust names are case-sensitive: `Bytes` is not the method `bytes`, `Ok` not a function `ok`.
const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind) && !(typeRef && !canNameInTypePosition(n)) &&
!(rustBare && (n.name !== ref.referenceName || !isRustNameInScope(n, ref, context))))
.filter((n) => (ref.referenceKind !== 'references' && ref.referenceKind !== 'function_ref') ||
sameLanguageFamily(n.language, ref.language));