mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 16:57:09 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user