fix(rust): a private item, or one another crate holds, no longer takes a name a file imports (#2692)

A Rust type alias declared without pub recorded no visibility in either
engine, so name matching took dioxus's private type Result in
src/build/patch.rs for the Result<...> a file means through
use crate::Result. Type aliases now record it, as functions and structs
already do, and a private module item is out of scope outside its own
module ahead of the ranking. The pipeline's veto already rejected such
a pick after the ranking, which left the reference unresolved: that is
what kept tokio's timeout(...), split(...) and sleep(...) calls from
linking, since a private namesake outranked the function they import.

With the private items gone, a runner-up from an unrelated crate could
win instead: tokio's tests' spawn(...) calls, under
use tokio_test::task::spawn, went to the examples crate's pub fn spawn.
So a name a use binds is now an item of the crate its path starts from,
or of a project crate that one's [dependencies] name, in turn, which a
facade's re-exports need: clap::Command is clap_builder's. crate::,
self:: and super:: start from the file's own crate; a named root counts
when the file's own manifest lists it. Dependencies are found by path
(their own, or the workspace's for workspace = true), so renamed keys
and crates outside the members land, and dotted keys such as
helper.workspace = true are read. Dev and build dependencies are not
re-exported, so they don't count: tokio's own use crate::spawn, whose
spawn sits in a macro the index doesn't see, stays unresolved instead
of going to tokio-test's. A file in no crate the manifests name, or a
path from anything else, is matched as before.

reference-target-kind's sibling-crate fixture re-exported pupil_core
without depending on it, which Cargo rejects; it now declares it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-20 00:44:52 +00:00
committed by GitHub
parent 40987d7776
commit 65d734236e
6 changed files with 385 additions and 22 deletions
+1
View File
@@ -207,6 +207,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 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
+2 -1
View File
@@ -114,7 +114,8 @@ describe('reference target-kind gate', () => {
write('core/Cargo.toml', `[package]\nname = "pupil_core"\nversion = "0.1.0"\n`);
write('core/src/lib.rs', `pub mod ports;\n`);
write('core/src/ports.rs', `pub trait CacheStore {\n fn get(&self);\n}\n`);
write('app/Cargo.toml', `[package]\nname = "app"\nversion = "0.1.0"\n`);
// Re-exporting pupil_core's module takes a dependency on it, as Cargo requires.
write('app/Cargo.toml', `[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\npupil_core = { path = "../core" }\n`);
write('app/src/lib.rs', `pub use pupil_core::ports;\n\npub mod platform;\n`);
write(
'app/src/platform.rs',
+164
View File
@@ -0,0 +1,164 @@
/**
* A private Rust item — no `pub` of any form — can be named only in its own
* module and the modules inside it. dioxus's CLI declares a private `type
* Result<T, E = PatchError>` in `src/build/patch.rs` beside the crate's own
* `pub(crate) type Result<T, E = Error>` in `error.rs`, which 21 files bring
* in with `use crate::Result`. Both read as in scope (`Result` is a prelude
* name, and the file `use`s one), and the private alias took 158 of those
* references. Type aliases now record their visibility, as functions and
* structs already did, in both engines, and a private item outside its
* module is out of scope.
*
* And a name a `use` binds is an item of the crate its path starts from, or
* of a project crate that one depends on. Once tokio's private `fn spawn` in
* `io/blocking.rs` stopped outranking the rest, its tests' `use
* tokio_test::task::spawn` calls went to the examples crate's `pub fn spawn`.
*/
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';
/** Writes `files` into a fresh temp dir and indexes it. */
async function indexProject(prefix: string, files: Record<string, string>): Promise<{ root: string; cg: CodeGraph }> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
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);
}
return { root, cg: await CodeGraph.init(root, { index: true }) };
}
/** What `name`, written in `file`, links to, as `file@line` of each target, sorted. */
const targetsOf = (cg: CodeGraph, file: string, name: string, kinds: string[]): string[] => {
const out: string[] = [];
for (const node of cg.getNodesInFile(file)) {
for (const edge of cg.getOutgoingEdges(node.id)) {
if (!kinds.includes(edge.kind)) continue;
const target = cg.getNode(edge.target)!;
if (target.name === name) out.push(`${target.filePath}@${target.startLine}`);
}
}
return [...new Set(out)].sort();
};
describe('a private Rust item outside its module', () => {
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
({ root, cg } = await indexProject('cg-rust-private-', {
'Cargo.toml': '[package]\nname = "cli"\nversion = "0.1.0"\n',
'src/main.rs': 'mod error;\nmod codegen;\nmod serve;\nmod watch;\n\npub(crate) use error::*;\npub(crate) use codegen::*;\n\nfn main() {}\n',
'src/error.rs': 'pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;\n\npub struct Error;\n',
'src/codegen/mod.rs': 'mod patch;\n\npub(crate) use patch::*;\n',
'src/codegen/patch.rs': [
'mod jump;',
'',
'type Result<T, E = PatchError> = std::result::Result<T, E>;',
'',
'pub struct PatchError;',
'',
'fn checksum(bytes: &[u8]) -> u32 {',
' bytes.len() as u32',
'}',
'',
'pub fn apply(bytes: &[u8]) -> Result<u32> {',
' Ok(checksum(bytes))',
'}',
'',
].join('\n'),
// A module inside `patch` sees its private items.
'src/codegen/patch/jump.rs': 'use super::*;\n\npub fn jump(bytes: &[u8]) -> Result<u32> {\n Ok(checksum(bytes))\n}\n',
'src/serve.rs': 'use crate::Result;\n\npub fn serve() -> Result<()> {\n Ok(())\n}\n',
'src/watch.rs': 'use crate::{Error, Result};\n\npub fn watch() -> Result<(), Error> {\n Ok(())\n}\n\nfn checksum(bytes: &[u8]) -> u32 {\n bytes.len() as u32\n}\n\npub fn scan(bytes: &[u8]) -> u32 {\n checksum(bytes)\n}\n',
}));
}, 60_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
it('records the visibility of a type alias', () => {
const aliases = (file: string) => cg.getNodesInFile(file).filter((n) => n.kind === 'type_alias').map((n) => n.visibility);
expect(aliases('src/error.rs')).toEqual(['public']);
expect(aliases('src/codegen/patch.rs')).toEqual(['private']);
});
it('is out of scope: `use crate::Result` names the crate\'s own alias', () => {
expect(targetsOf(cg, 'src/serve.rs', 'Result', ['references'])).toEqual(['src/error.rs@1']);
expect(targetsOf(cg, 'src/watch.rs', 'Result', ['references'])).toEqual(['src/error.rs@1']);
});
it('stays in scope in its own module and the modules inside it', () => {
expect(targetsOf(cg, 'src/codegen/patch.rs', 'Result', ['references'])).toEqual(['src/codegen/patch.rs@3']);
expect(targetsOf(cg, 'src/codegen/patch/jump.rs', 'Result', ['references'])).toEqual(['src/codegen/patch.rs@3']);
expect(targetsOf(cg, 'src/codegen/patch/jump.rs', 'checksum', ['calls'])).toEqual(['src/codegen/patch.rs@7']);
});
it('keeps a private function to its module too', () => {
expect(targetsOf(cg, 'src/watch.rs', 'checksum', ['calls'])).toEqual(['src/watch.rs@7']);
expect(targetsOf(cg, 'src/codegen/patch.rs', 'checksum', ['calls'])).toEqual(['src/codegen/patch.rs@7']);
});
});
describe('a name a Rust `use` binds, across a workspace', () => {
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
({ root, cg } = await indexProject('cg-rust-use-crate-', {
'Cargo.toml': '[workspace]\nmembers = ["app", "examples", "facade", "helper"]\n\n[workspace.dependencies]\nhelper = { path = "helper" }\n',
// helper through the workspace's table, written as a dotted key.
'app/Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\n\n[dependencies]\nfacade = { path = "../facade" }\n\n[dev-dependencies]\nhelper.workspace = true\n',
'app/src/lib.rs': 'mod io;\n',
'app/src/io/mod.rs': 'mod blocking;\n',
// A private namesake nearer the test than the crate it names.
'app/src/io/blocking.rs': 'fn spawn<F: FnOnce()>(f: F) {\n f()\n}\n\npub(crate) fn run() {\n spawn(|| {});\n}\n',
'app/tests/notify.rs': [
'use facade::Thing;',
'use helper::task::spawn;',
'',
'#[test]',
'fn notified() {',
' let _task = spawn(());',
'}',
'',
'fn keep(thing: Thing) -> Thing {',
' thing',
'}',
'',
].join('\n'),
// Nothing depends on the examples crate: its `pub fn spawn` is no one else's.
'examples/Cargo.toml': '[package]\nname = "examples"\nversion = "0.1.0"\n\n[dependencies]\napp = { path = "../app" }\n',
'examples/custom.rs': 'mod runtime {\n pub fn spawn<T>(task: T) -> T {\n task\n }\n}\n\nfn main() {\n runtime::spawn(());\n}\n',
// A facade re-exports a crate it depends on: renamed, and no member of the workspace.
'facade/Cargo.toml': '[package]\nname = "facade"\nversion = "0.1.0"\n\n[dependencies]\nshared = { package = "shared-types", path = "../types" }\n',
'facade/src/lib.rs': 'pub use shared::Thing;\n',
'helper/Cargo.toml': '[package]\nname = "helper"\nversion = "0.1.0"\n',
'helper/src/lib.rs': 'pub mod task;\n',
'helper/src/task.rs': 'pub fn spawn<T>(task: T) -> T {\n task\n}\n',
'types/Cargo.toml': '[package]\nname = "shared-types"\nversion = "0.1.0"\n',
'types/src/lib.rs': 'pub struct Thing;\n',
}));
}, 60_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
it('is an item of the crate its path names: `use helper::task::spawn` is helper\'s', () => {
expect(targetsOf(cg, 'app/tests/notify.rs', 'spawn', ['calls'])).toEqual(['helper/src/task.rs@1']);
});
it('or one that crate re-exports from a crate it depends on', () => {
expect(targetsOf(cg, 'app/tests/notify.rs', 'Thing', ['references'])).toEqual(['types/src/lib.rs@1']);
});
it('leaves a private item to its own module', () => {
expect(targetsOf(cg, 'app/src/io/blocking.rs', 'spawn', ['calls'])).toEqual(['app/src/io/blocking.rs@1']);
});
});
+3
View File
@@ -688,6 +688,9 @@ impl<'t> Walker<'t> {
}
let extra = Extra {
docstring: preceding_docstring(node, self.src),
// Private unless `pub` (any form), as the wasm walker reads it:
// the resolver keeps a private alias to its own module.
visibility: Some(self.visibility_of(node)),
..Extra::default()
};
self.create_node("type_alias", &name, node, extra);
+3
View File
@@ -3980,6 +3980,9 @@ export class TreeSitterExtractor {
const typeAliasNode = this.createNode('type_alias', name, node, {
docstring,
isExported,
// A Rust alias is private to its module unless `pub` (any form): the
// resolver keeps a private one to its own module (isRustNameInScope).
...(this.language === 'rust' ? { visibility: this.extractor.getVisibility?.(node) } : {}),
});
// Extract type references from the alias value (e.g., `type X = ITextModel | null`)
+212 -21
View File
@@ -7493,44 +7493,205 @@ interface RustUses {
external: Set<string>;
/** The items the file's project `use`s bind — their leaves, not the paths they walk. */
bound: Set<string>;
/** Each bound item → the first segment of every path that binds it (`crate`, `super`, `tokio_test`). */
boundFrom: Map<string, Set<string>>;
}
const RUST_CRATES = new WeakMap<ResolutionContext, Set<string>>();
const RUST_DEPENDENCIES = new WeakMap<ResolutionContext, Set<string>>();
interface RustManifestDependency {
/** The name code writes for it (`futures_util`). */
key: string;
/** The package it is: its key, unless `package = "…"` renames it. */
pkg: string;
/** Its `path = "…"`, from the manifest's directory. */
path?: string;
/** `workspace = true`: the workspace's `[workspace.dependencies]` declares it. */
workspace: boolean;
/** `[dependencies]` (per-target ones included), dev, build, or `[workspace.dependencies]`. */
kind: 'normal' | 'dev' | 'build' | 'workspace';
}
/**
* The crates the project's manifests depend on (`[dependencies]`,
* `[dev-dependencies]`, `[build-dependencies]`, per-target ones), by the name
* code writes them (`futures_util`), less the project's own.
* A Cargo manifest's dependencies, however each is written: `foo = "1"`, an
* inline table, dotted keys (`foo.workspace = true`) or a `[dependencies.foo]`
* table of its own.
*/
function rustManifestDependencies(text: string): RustManifestDependency[] {
const deps = new Map<string, RustManifestDependency>();
const entry = (kind: RustManifestDependency['kind'], name: string): RustManifestDependency => {
const key = name.replace(/-/g, '_');
let dep = deps.get(`${kind} ${key}`);
if (!dep) deps.set(`${kind} ${key}`, (dep = { key, pkg: key, workspace: false, kind }));
return dep;
};
const read = (dep: RustManifestDependency, fields: string): void => {
const pkg = /\bpackage\s*=\s*"([^"]+)"/.exec(fields)?.[1];
if (pkg) dep.pkg = pkg.replace(/-/g, '_');
const at = /\bpath\s*=\s*"([^"]+)"/.exec(fields)?.[1];
if (at) dep.path = at.replace(/\\/g, '/');
if (/\bworkspace\s*=\s*true\b/.test(fields)) dep.workspace = true;
};
let kind: RustManifestDependency['kind'] | null = null;
let table: RustManifestDependency | null = null;
for (const raw of text.split(/\r?\n/)) {
const line = raw.replace(/#.*$/, '').trim();
const header = /^\[([^\]]+)\]$/.exec(line);
if (header) {
const m = /^(?:(workspace)\.|target\..+\.)?(dev-|build-)?dependencies(?:\.([A-Za-z0-9_-]+))?$/.exec(header[1]!.trim());
const of = !m ? null : m[1] ? 'workspace' : m[2] === 'dev-' ? 'dev' : m[2] === 'build-' ? 'build' : 'normal';
kind = of !== null && !m![3] ? of : null;
table = of !== null && m![3] ? entry(of, m![3]) : null;
continue;
}
if (table) {
read(table, line);
continue;
}
const dep = kind === null ? null : /^"?([A-Za-z0-9_-]+)"?\s*(?:\.\s*([A-Za-z_-]+)\s*)?=\s*(.*)$/.exec(line);
if (dep) read(entry(kind!, dep[1]!), dep[2] ? `${dep[2]} = ${dep[3]}` : dep[3]!);
}
return [...deps.values()];
}
/** The crates the project's manifests depend on, by the name code writes them (`futures_util`), less the project's own. */
function rustDependencyCrates(context: ResolutionContext): Set<string> {
const hit = RUST_DEPENDENCIES.get(context);
if (hit) return hit;
const deps = new Set<string>();
const manifests = ['Cargo.toml', ...[...getCargoWorkspaceCrateMap(context).values()].map((dir) => `${dir}/Cargo.toml`)];
for (const manifest of new Set(manifests)) {
const text = context.readFile(manifest) ?? '';
let inDeps = false;
for (const raw of text.split(/\r?\n/)) {
const line = raw.replace(/#.*$/, '').trim();
const header = /^\[([^\]]+)\]$/.exec(line);
if (header) {
const table = header[1]!.trim();
// `[dependencies.tokio]` names one dependency in its header.
const named = /(?:^|\.)(?:dev-|build-)?dependencies\.([A-Za-z0-9_-]+)$/.exec(table);
if (named) deps.add(named[1]!.replace(/-/g, '_'));
inDeps = /(?:^|\.)(?:dev-|build-)?dependencies$/.test(table);
continue;
}
const key = inDeps ? /^([A-Za-z0-9_-]+)\s*=/.exec(line)?.[1] : undefined;
if (key) deps.add(key.replace(/-/g, '_'));
}
for (const { key } of rustManifestDependencies(context.readFile(manifest) ?? '')) deps.add(key);
}
for (const own of rustProjectCrates(context)) deps.delete(own);
RUST_DEPENDENCIES.set(context, deps);
return deps;
}
interface RustCrate {
/** Its package name as code writes it (`tokio_test`). */
name: string;
/** Each dependency of any kind that is a project crate: the name code writes → that crate's directory. */
keys: Map<string, string>;
/** The project crates its `[dependencies]` name, by directory: the ones it can re-export from. */
deps: Set<string>;
}
interface RustCrateGraph {
/** The project's crates by directory: the root package's is ''. */
crates: Map<string, RustCrate>;
/** A crate's directory → itself and every project crate it depends on, transitively. */
reach: Map<string, Set<string>>;
/** A file → the directory of the crate it is in, or null for none the manifests name. */
fileCrate: Map<string, string | null>;
}
const RUST_CRATE_GRAPHS = new WeakMap<ResolutionContext, RustCrateGraph>();
/**
* The project's crates — the root package, the workspace's members and the
* path dependencies they name — and which of them each depends on. A
* dependency is found by its `path` (its own or, for `workspace = true`, the
* workspace's), so a renamed key or a crate outside the members still lands;
* without one, by its package name.
*/
function rustCrateGraph(context: ResolutionContext): RustCrateGraph {
const hit = RUST_CRATE_GRAPHS.get(context);
if (hit) return hit;
const crates = new Map<string, RustCrate>();
const manifests = new Map<string, string>();
const byName = new Map<string, string>();
const add = (dir: string): boolean => {
if (crates.has(dir)) return true;
const text = context.readFile(dir === '' ? 'Cargo.toml' : `${dir}/Cargo.toml`);
const name = text ? /\[package\][^[]*?\bname\s*=\s*"([^"]+)"/.exec(text)?.[1]?.replace(/-/g, '_') : undefined;
if (!name) return false;
crates.set(dir, { name, keys: new Map(), deps: new Set() });
manifests.set(dir, text!);
if (!byName.has(name)) byName.set(name, dir);
return true;
};
add('');
for (const dir of getCargoWorkspaceCrateMap(context).values()) add(dir.replace(/^\.(?:\/|$)/, ''));
const inherited = new Map(rustManifestDependencies(context.readFile('Cargo.toml') ?? '')
.filter((dep) => dep.kind === 'workspace').map((dep) => [dep.key, dep]));
// A path from `dir`, as a project directory; null when it leaves the project.
const join = (dir: string, rel: string): string | null => {
const joined = path.posix.normalize(dir === '' ? rel : `${dir}/${rel}`).replace(/\/+$/, '');
return joined === '.' ? '' : joined === '..' || joined.startsWith('../') || path.posix.isAbsolute(joined) ? null : joined;
};
const queue = [...crates.keys()];
for (let i = 0; i < queue.length; i++) {
const dir = queue[i]!;
const crate = crates.get(dir)!;
for (const dep of rustManifestDependencies(manifests.get(dir)!)) {
if (dep.kind === 'workspace') continue;
const shared = dep.workspace ? inherited.get(dep.key) : undefined;
const at = shared ? (shared.path === undefined ? null : join('', shared.path)) : dep.path === undefined ? null : join(dir, dep.path);
let to: string | undefined;
if (at !== null && !crates.has(at) && add(at)) queue.push(at);
if (at !== null && crates.has(at)) to = at;
else to = byName.get(shared?.pkg ?? dep.pkg);
if (to === undefined || to === dir) continue;
crate.keys.set(dep.key, to);
if (dep.kind === 'normal') crate.deps.add(to);
}
}
const reach = new Map<string, Set<string>>();
for (const dir of crates.keys()) {
const seen = new Set([dir]);
const pending = [dir];
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
for (const to of crates.get(next)!.deps) {
if (seen.has(to)) continue;
seen.add(to);
pending.push(to);
}
}
reach.set(dir, seen);
}
const graph = { crates, reach, fileCrate: new Map<string, string | null>() };
RUST_CRATE_GRAPHS.set(context, graph);
return graph;
}
/** The directory of the crate `filePath` is in: the deepest one that holds it, the root package's (''), or null. */
function rustCrateDirOf(filePath: string, graph: RustCrateGraph): string | null {
const hit = graph.fileCrate.get(filePath);
if (hit !== undefined) return hit;
let best: string | null = graph.crates.has('') ? '' : null;
for (const dir of graph.crates.keys()) {
if (dir !== '' && filePath.startsWith(`${dir}/`) && (best === null || dir.length > best.length)) best = dir;
}
graph.fileCrate.set(filePath, best);
return best;
}
/**
* Whether a `use` path from one of `roots` can name this candidate, by its
* crate. A path from a crate the file's own manifest depends on (`use
* tokio_test::task::spawn`), or from its own crate's name, names that crate's
* item or one it re-exports from a crate its `[dependencies]` name, in turn
* (`clap::Command` is clap_builder's); `crate::`, `self::` and `super::` start
* from the file's own crate. tokio's tests' 133 `spawn(…)` calls are
* tokio-test's, never the examples crate's `pub fn spawn`, which nothing
* depends on. Any other path — through a module, a crate from outside the
* project — and a file in no crate the manifests name say nothing.
*/
function rustUseCanReach(roots: Set<string>, candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
const graph = rustCrateGraph(context);
const at = rustCrateDirOf(candidate.filePath, graph);
const own = rustCrateDirOf(ref.filePath, graph);
if (at === null || own === null) return true;
const crate = graph.crates.get(own)!;
for (const root of roots) {
const from = root === 'crate' || root === 'self' || root === 'super' || root === crate.name ? own : crate.keys.get(root);
if (from === undefined || graph.reach.get(from)!.has(at)) return true;
}
return false;
}
/** The project's own crate names (`tokio`, `tokio_util`), from its Cargo.toml files. */
function rustProjectCrates(context: ResolutionContext): Set<string> {
const hit = RUST_CRATES.get(context);
@@ -7553,7 +7714,7 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
}
const hit = memo.get(filePath);
if (hit) return hit;
const uses: RustUses = { names: new Set(), globs: new Set(), external: new Set(), bound: new Set() };
const uses: RustUses = { names: new Set(), globs: new Set(), external: new Set(), bound: new Set(), boundFrom: new Map() };
const leaves = (tree: string): string[] => [
...[...tree.matchAll(/([A-Za-z_]\w*)\s*(?=[,}]|$|\s+as\b)|\bas\s+([A-Za-z_]\w*)/g)]
.map((leaf) => leaf[2] ?? leaf[1]!).filter((id) => id !== 'self' && id !== 'as'),
@@ -7574,7 +7735,12 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
for (const id of leaves(tree)) uses.external.add(id);
continue;
}
for (const id of leaves(tree)) uses.bound.add(id);
for (const id of leaves(tree)) {
uses.bound.add(id);
const from = uses.boundFrom.get(id);
if (from) from.add(root);
else uses.boundFrom.set(id, new Set([root]));
}
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]!);
}
@@ -7589,6 +7755,24 @@ function rustModuleName(filePath: string): string {
return base === 'mod' || base === 'lib' || base === 'main' ? parts[parts.length - 2] ?? base : base;
}
/** Rust items declared in a module, whose own `pub` (or its absence) says who can name them. */
const RUST_MODULE_ITEM_KINDS = new Set(['function', 'struct', 'enum', 'union', 'type_alias', 'trait', 'constant', 'variable']);
/**
* Whether `ref`'s file can name this Rust item. A private one — no `pub` of
* any form — is named only in its own module and the modules inside it: a
* `type Result` in `src/build/patch.rs` is `build::patch`'s, seen in that file
* and under `src/build/patch/`, never through `use crate::Result` elsewhere,
* and no glob passes it on (dioxus's `pub(crate) use patch::*` doesn't). A
* `mod.rs`, `lib.rs` or `main.rs` is the module of its whole directory. The
* same rule isVisibleAcrossFiles applies to the pipeline's pick, here ahead of
* the ranking, so the crate's own `pub(crate) type Result` can win instead.
*/
function isRustItemVisibleFrom(candidate: Node, ref: UnresolvedRef): boolean {
if (candidate.language !== 'rust' || candidate.visibility !== 'private' || !RUST_MODULE_ITEM_KINDS.has(candidate.kind)) return true;
return candidate.filePath === ref.filePath || ref.filePath.startsWith(`${rustModuleDir(candidate.filePath)}/`);
}
/** 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;
@@ -7606,8 +7790,11 @@ function rustGlobCovers(uses: RustUses, candidate: Node, ref: UnresolvedRef): bo
* `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.
* A private item is out of scope outside its module (isRustItemVisibleFrom),
* and an item a `use` binds is one its path's crate can reach (rustUseCanReach).
*/
export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (!isRustItemVisibleFrom(candidate, ref)) return false;
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.
@@ -7663,6 +7850,9 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context:
}
if (candidate.filePath === ref.filePath) return true;
const uses = rustUsesOf(ref.filePath, context);
// An item a `use` binds is one its path's crate can name.
const from = RUST_MODULE_ITEM_KINDS.has(candidate.kind) ? uses.boundFrom.get(name) : undefined;
if (from && !rustUseCanReach(from, candidate, ref, context)) return false;
// `use std::task::{Context, Poll}`: the file's `Context` is std's, not tokio's
// `runtime::context::Context`. (A method call `.env(…)` is no imported name.)
if (uses.external.has(name) && !uses.bound.has(name) && line !== undefined) {
@@ -13199,6 +13389,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
RUST_USES.delete(context);
RUST_CRATES.delete(context);
RUST_DEPENDENCIES.delete(context);
RUST_CRATE_GRAPHS.delete(context);
LEXICAL_SCOPE_MEMO.delete(context);
KOTLIN_LAMBDA_RECEIVERS.delete(context);
SCALA_IMPORTED_SUPERS.delete(context);