diff --git a/CHANGELOG.md b/CHANGELOG.md index 57d116475..b12da8fd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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(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 diff --git a/__tests__/reference-target-kind.test.ts b/__tests__/reference-target-kind.test.ts index 6eb421174..24f58f06a 100644 --- a/__tests__/reference-target-kind.test.ts +++ b/__tests__/reference-target-kind.test.ts @@ -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', diff --git a/__tests__/rust-private-item-scope.test.ts b/__tests__/rust-private-item-scope.test.ts new file mode 100644 index 000000000..0a1d261be --- /dev/null +++ b/__tests__/rust-private-item-scope.test.ts @@ -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` in `src/build/patch.rs` beside the crate's own + * `pub(crate) type Result` 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): 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 = std::result::Result;\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 = std::result::Result;', + '', + 'pub struct PatchError;', + '', + 'fn checksum(bytes: &[u8]) -> u32 {', + ' bytes.len() as u32', + '}', + '', + 'pub fn apply(bytes: &[u8]) -> Result {', + ' 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 {\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: 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(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(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']); + }); +}); diff --git a/codegraph-kernel/src/rustlang.rs b/codegraph-kernel/src/rustlang.rs index 64b8923dd..089d6a758 100644 --- a/codegraph-kernel/src/rustlang.rs +++ b/codegraph-kernel/src/rustlang.rs @@ -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); diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 0824c5d26..cf07a4080 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -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`) diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index abfc090f8..72b27f3f9 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -7493,44 +7493,205 @@ interface RustUses { external: Set; /** The items the file's project `use`s bind — their leaves, not the paths they walk. */ bound: Set; + /** Each bound item → the first segment of every path that binds it (`crate`, `super`, `tokio_test`). */ + boundFrom: Map>; } const RUST_CRATES = new WeakMap>(); const RUST_DEPENDENCIES = new WeakMap>(); +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(); + 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 { const hit = RUST_DEPENDENCIES.get(context); if (hit) return hit; const deps = new Set(); 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; + /** The project crates its `[dependencies]` name, by directory: the ones it can re-export from. */ + deps: Set; +} + +interface RustCrateGraph { + /** The project's crates by directory: the root package's is ''. */ + crates: Map; + /** A crate's directory → itself and every project crate it depends on, transitively. */ + reach: Map>; + /** A file → the directory of the crate it is in, or null for none the manifests name. */ + fileCrate: Map; +} + +const RUST_CRATE_GRAPHS = new WeakMap(); + +/** + * 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(); + const manifests = new Map(); + const byName = new Map(); + 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>(); + 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() }; + 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, 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 { 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);