mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 16:52:09 +08:00
fix(rust): a module a #[path] attribute loads is the module that declares it (#3064)
A Rust module loaded with `#[path = "…"]` was found by its file's directory:
fast-mlsirm's `#[path = "../../../tests/unit/scaling_tests.rs"] mod tests;`
in `crates/core/src/scaling.rs` made `tests/unit/scaling_tests.rs`'s
`use super::*` walk to a `tests/unit` module that doesn't exist, so its
`predict_rating_multi(1)` linked to nothing, or to a same-named function of
another crate; its `crate::` had no crate, and the parent's private items
looked out of its reach. From the declaring side, a path through such a
module (`utils::mock_event(…)` under `#[path = "./utils.rs"] mod utils;`)
reached nothing.
The fix reads each file's `#[path]` declarations as the Rust Reference
reads the attribute (from the declaring file's directory, or inside an
inline `mod` from that module's own; a path that leaves the project, by
escapesRoot, or that a `cfg_attr` sets, names no file):
- rustSubmodule: a module the module declares once with a `#[path]` is
the file it names, whatever else loads that file too (a helper several
tests load is each test's); cfg alternatives that name a file each stay
unknown, and a path's 2015 crate-root fallback no longer reaches past
such a module to a namesake at the crate root.
- rustPathClaim: a file the regular rules don't place is the module of the
one `#[path]` declaration that loads it, if exactly one does. Placed
(rustPlacedByName) are crate roots, files with a `fn main` or
`#![no_main]` of their own (trybuild cases, fuzz targets), and files a
plain `mod name;` declares in the module owning their directory or in an
inline module of one above it (dioxus's `mod components { pub mod home; }`
in main.rs). Only for a file left over are the project's Rust files that
write `path =` read (a byte check first), once per resolver. With a
claim, rustSuperModule is the declaring module, rustFileModule owns the
file's directory (rustc reads every `#[path]` file as a mod.rs),
rustCrateRootModule is the declaring file's crate, and rustModuleWithin,
isRustItemVisibleFrom and isVisibleAcrossFiles see the file inside the
declaring module, so `use super::*` brings in its private items.
- rustCrateGraph: `[[example]]`, `[[test]]` and `[[bench]]` paths are
target roots, as `[lib]`/`[[bin]]` ones are since PR #3014, and such an
example is its own crate's root (dioxus's
`examples/07-fullstack/handling_errors.rs`).
Validation:
- rust-path-super-glob.test.ts (PR 1892's case, credited) and
rust-path-attribute-modules.test.ts (7 cases): on main all 8 fail; all
pass. Every rust-* suite passes.
- A/B against main, native kernel: fast-mlsirm (its Rust files) +2,796
links (1,839 calls, 427 references, 334 constructions, 196 imports), 71
retargeted (exposure_tests.rs's `Lcg::next_f64` from cdm_tests.rs's
private `Lcg` to exposure.rs's, the declaring module's helpers instead
of bifactor_grm.rs's), 87 reaching the same item through the declaring
module (confidence 0.4 to 0.9), 0 removed; dioxus +27 (each headless
test's `utils::mock_event(…)` into the helper five tests load); tokio +2
(`use super::AtomicU64` in the files atomic_u64_as_mutex.rs loads);
ripgrep, axum, clap, serde, gleam, express and flask unchanged.
- Cost, native kernel, medians of 3 interleaved runs: fast-mlsirm CPU 15.3
-> 15.8 s, wall 4.2 -> 5.2 s, peak RSS 825 -> 882 MB; tokio CPU 26.1 ->
26.7 s, wall 10.4 -> 11.2 s, peak RSS 1,023 -> 1,028 MB; dioxus CPU 34.8
-> 35.6 s, wall 10.3 -> 11.2 s, peak RSS 1,125 -> 1,120 MB; gleam CPU
24.1 -> 23.8 s, wall 6.6 -> 6.7 s, peak RSS 1,027 -> 1,023 MB.
Builds on PR #1892 by @seonghobae.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ed6b88e53a
commit
cc17978174
@@ -136,6 +136,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Rust, a call written through a module path, like `crate::util::take(3)`, `super::util::take(3)` or `util::take(3)`, and a `use` of a function, like `use crate::util::take;`, now link to the function that module declares. Before, they could land on a method of the same name declared above it in that file, like an `impl Buf`'s `take`, or on a function written inside another function, and a call through a module the standard library provides, like `fs::read(…)` under `use std::fs;`, could link to your own `fs` module's `read`. Re-index Rust projects after upgrading. Thanks @danusha2345. (#2308)
|
||||
- In Rust, a name a `use` renames, like `consume()` after `use crate::util::take as consume;`, now links to the item its path names, and a `use` written inside a function or a block counts only there, ahead of the file's own `use`s and items, so `fn first() { use crate::util::take; take() }` reaches util's `take` and the next function's own `use` its own. A call to a parameter, a closure or a local that shares a function's name, like `take()` inside `fn run(take: fn())` or after `let take = || {};`, no longer links to that function, while `let test = test::<i8>;` still calls `test`, and a `use` of an enum's variant, like `use crate::models::Mode::Fast;`, links `Fast(…)` to that enum's variant. A path through a module, like `super::helper()`, `crate::config()`, `super::shared::helper()` inside a `mod tests { … }` or `tokio::task::yield_now()`, now reaches the item that module declares or passes on with a `pub use`, in your own crate or another crate of the workspace, and its `use` links to it; that includes binaries whose manifest gives them a path of their own, like ripgrep's, and packages whose manifest lists `name` after other fields, like axum-core's. A `use` written inside a string no longer counts. Re-index Rust projects after upgrading. Thanks @danusha2345. (#2308)
|
||||
- In Rust, a call through a type that a path or a `use` names, like `sim::items::ItemPool::for_def(…)` into another crate of your workspace, `ItemPool::for_def(…)` under `use sim::items::ItemPool;` or `crate::models::Mode::Fast(1)`, now links to that type's own function or variant, even when a type of the same name in another crate or module has one too, and a `use` of a `pub const` or `static`, like `use sim::RANK_COUNT;`, links to it. A type written on the line below its module path, as rustfmt wraps a long signature (`scheduler::` then `Handle`), now links by that path rather than by a `use` of its name. A workspace member listed outside the project, like `../shared`, is no longer read. Re-index Rust projects after upgrading. Thanks @BGamboa13. (#2631)
|
||||
- In Rust, a module loaded with `#[path = "…"]`, like tests kept in another folder as `#[path = "../../../tests/unit/scaling_tests.rs"] mod tests;`, is now part of the module that declares it: its `use super::*` brings in that module's items, private ones included, `super::` and `crate::` start from there, and the path is read as Rust reads it, from the declaring file's folder or an inline module's own. A call through such a module, like `support::make()` in each test that loads a shared helper, reaches the file it names. A module whose `#[cfg]` alternatives each name a file of their own, or whose path a `cfg_attr` sets, depends on the build and is left alone. An example, test or bench that `Cargo.toml` places at a path of its own, like `examples/07-fullstack/handling_errors.rs`, is now its own crate, so its `crate::` starts from it. Re-index Rust projects after upgrading. Thanks @seonghobae. (#1892)
|
||||
- Axum routes now link to their handler when rustfmt wraps the `.route(` call so the handler sits on a later line, and when the handler's name also appears elsewhere on the line, as in `delete(handlers::user::delete)` or `.route("/login", post(handlers::user::login))`; Actix's `web::resource(…)` and `.route(…)` registrations get the same fix. A call inside a closure handler is no longer mistaken for a route, and an Actix resource no longer takes the next route's handler as its own. Thanks @mg-mg-mg for the report and @danusha2345. (#2326)
|
||||
- In Play projects, a route or a call like `MediaType.parse(...)` now links to a method of the class or object it names, and a call to an overloaded one to the overload its arguments fit. Before, it went to the first method of that name anywhere in the class's file, often a nested or sibling class's. When a Scala and a Java type share a name, as Play's own `RequestAuthority` does, each language's calls now reach their own. Thanks @danusha2345.
|
||||
- In Spring projects, a route now links to the method its mapping annotation is written on when that method has no access modifier, like `@GetMapping List<Article> list()` in a package-private controller, and a method-level `@RequestMapping(value = "/feed", method = GET)` on such a method now makes a route. Before, such a handler was skipped, so its route linked the next method declared `public`, `private` or `protected`, or nothing, and the handler looked unused. An overloaded handler under a tall stack of annotations, like `@Operation` and `@ApiResponses` above `@GetMapping`, now links the overload it annotates rather than its neighbour. Re-index Spring projects after upgrading. Thanks @Liurchen.
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* A Rust module loaded with `#[path = "…"]` is the module that declares it
|
||||
* (the Rust Reference, "The path attribute"): its `super` is the declaring
|
||||
* module, wherever the file lies, so `use super::*` brings in that module's
|
||||
* items, private ones included, and `crate::` is the declaring file's crate.
|
||||
* The path is read from the declaring file's directory, or from an inline
|
||||
* module's own directory. A module whose cfg alternatives each name a file,
|
||||
* or whose path a `cfg_attr` sets, depends on the build and stays unread.
|
||||
* A file several declarations load is each one's from its own side, and an
|
||||
* example the manifest places at a path of its own is its own crate's root.
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
let root = '';
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-path-attr-'));
|
||||
const files: Record<string, string> = {
|
||||
'Cargo.toml': '[workspace]\nmembers = ["crates/core", "crates/py"]\nresolver = "2"\n',
|
||||
'crates/core/Cargo.toml': '[package]\nname = "core-crate"\nversion = "0.1.0"\nedition = "2021"\n\n[[example]]\nname = "demo"\npath = "examples/nested/demo.rs"\n',
|
||||
'crates/core/src/lib.rs': 'pub mod scaling;\npub mod platform;\npub mod util;\n\npub fn root_helper() -> i32 {\n 0\n}\n\npub mod gen_user;\n',
|
||||
'crates/core/src/util.rs': 'pub fn helper() -> i32 {\n 10\n}\n',
|
||||
// A folder whose name only starts with two dots is inside the project.
|
||||
'crates/core/src/gen_user.rs': '#[path = "..generated/gen.rs"]\nmod gen;\n\npub fn via_gen() -> i32 {\n gen::make_gen()\n}\n',
|
||||
'crates/core/src/..generated/gen.rs': 'pub fn make_gen() -> i32 {\n 13\n}\n',
|
||||
// An example the manifest places at a path of its own: its own crate, whose `mod util;` is its sibling.
|
||||
'crates/core/examples/nested/demo.rs': 'mod util;\n\nfn main() {\n let _ = crate::util::helper();\n}\n',
|
||||
'crates/core/examples/nested/util.rs': 'pub fn helper() -> i32 {\n 11\n}\n',
|
||||
// Two tests load one helper: from each, `support` is that file.
|
||||
'crates/core/tests/a.rs': '#[path = "support/helpers.rs"]\nmod support;\n\n#[test]\nfn uses_support() {\n let _ = support::make();\n}\n',
|
||||
'crates/core/tests/b.rs': '#[path = "support/helpers.rs"]\nmod support;\n\n#[test]\nfn uses_support_too() {\n let _ = support::make();\n}\n',
|
||||
'crates/core/tests/support/helpers.rs': 'pub fn make() -> i32 {\n 12\n}\n',
|
||||
'crates/core/src/scaling.rs': `pub fn predict_rating_multi(x: i32) -> i32 {
|
||||
x + 1
|
||||
}
|
||||
|
||||
fn private_helper() -> i32 {
|
||||
2
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../../tests/unit/scaling_tests.rs"]
|
||||
mod tests;
|
||||
`,
|
||||
'crates/core/src/platform.rs': `#[cfg(unix)]
|
||||
#[path = "platform/unix_impl.rs"]
|
||||
mod imp;
|
||||
#[cfg(windows)]
|
||||
#[path = "platform/windows_impl.rs"]
|
||||
mod imp;
|
||||
|
||||
pub fn open() -> i32 {
|
||||
3
|
||||
}
|
||||
|
||||
pub fn via_imp() -> i32 {
|
||||
imp::open_impl()
|
||||
}
|
||||
|
||||
mod inline {
|
||||
#[path = "other.rs"]
|
||||
mod inner;
|
||||
}
|
||||
|
||||
#[cfg_attr(docsrs, path = "docs_impl.rs")]
|
||||
mod documented;
|
||||
|
||||
pub fn via_documented() -> i32 {
|
||||
documented::run()
|
||||
}
|
||||
`,
|
||||
'crates/core/src/platform/unix_impl.rs': 'pub fn open_impl() -> i32 {\n 1\n}\n',
|
||||
'crates/core/src/platform/windows_impl.rs': 'pub fn open_impl() -> i32 {\n 2\n}\n',
|
||||
// In a non-mod-rs file's inline module, the path starts at that module's own directory.
|
||||
'crates/core/src/platform/inline/other.rs': 'pub fn from_inner() -> i32 {\n super::super::open()\n}\n',
|
||||
'crates/core/src/platform/other.rs': 'pub fn decoy() -> i32 {\n 4\n}\n',
|
||||
'crates/core/src/documented.rs': 'pub fn run() -> i32 {\n 5\n}\n',
|
||||
'crates/core/src/platform/docs_impl.rs': 'pub fn run() -> i32 {\n 6\n}\n',
|
||||
'crates/py/Cargo.toml': '[package]\nname = "py-crate"\nversion = "0.1.0"\nedition = "2021"\n',
|
||||
'crates/py/src/lib.rs': 'pub fn predict_rating_multi(x: i32) -> i32 {\n x + 99\n}\n\npub fn private_helper() -> i32 {\n 98\n}\n\npub fn root_helper() -> i32 {\n 97\n}\n\npub fn open() -> i32 {\n 96\n}\n\npub fn make() -> i32 {\n 95\n}\n',
|
||||
'tests/unit/scaling_tests.rs': `use super::*;
|
||||
|
||||
#[test]
|
||||
fn uses_private() {
|
||||
let _ = private_helper();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uses_crate() {
|
||||
let _ = crate::root_helper();
|
||||
}
|
||||
`,
|
||||
};
|
||||
for (const [relative, content] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, relative)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, relative), content);
|
||||
}
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cg?.close();
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const calls = (file: string, name: string): string[] => {
|
||||
const fn = cg.getNodesInFile(file).find((n) => n.name === name && n.kind === 'function');
|
||||
expect(fn, `${file}:${name}`).toBeDefined();
|
||||
return cg.getOutgoingEdgesFrom([fn!.id]).filter((e) => e.kind === 'calls')
|
||||
.map((e) => cg.getNode(e.target)!).map((t) => `${t.qualifiedName} ${t.filePath}:${t.startLine}`).sort();
|
||||
};
|
||||
|
||||
describe('Rust modules a #[path] attribute loads', () => {
|
||||
it('see the declaring module\'s private items through `use super::*`', () => {
|
||||
expect(calls('tests/unit/scaling_tests.rs', 'uses_private')).toEqual(['private_helper crates/core/src/scaling.rs:5']);
|
||||
});
|
||||
|
||||
it('start `crate::` from the declaring file\'s crate', () => {
|
||||
expect(calls('tests/unit/scaling_tests.rs', 'uses_crate')).toEqual(['root_helper crates/core/src/lib.rs:5']);
|
||||
});
|
||||
|
||||
it('read the path from an inline module\'s own directory', () => {
|
||||
expect(calls('crates/core/src/platform/inline/other.rs', 'from_inner')).toEqual(['open crates/core/src/platform.rs:8']);
|
||||
});
|
||||
|
||||
it('follow each declaring file into a helper several tests load', () => {
|
||||
expect(calls('crates/core/tests/a.rs', 'uses_support')).toEqual(['make crates/core/tests/support/helpers.rs:1']);
|
||||
expect(calls('crates/core/tests/b.rs', 'uses_support_too')).toEqual(['make crates/core/tests/support/helpers.rs:1']);
|
||||
});
|
||||
|
||||
it('read a path into a folder whose name only starts with two dots', () => {
|
||||
expect(calls('crates/core/src/gen_user.rs', 'via_gen')).toEqual(['make_gen crates/core/src/..generated/gen.rs:1']);
|
||||
});
|
||||
|
||||
it('start an example the manifest places at a path of its own from that example', () => {
|
||||
expect(calls('crates/core/examples/nested/demo.rs', 'main')).toEqual(['helper crates/core/examples/nested/util.rs:1']);
|
||||
});
|
||||
|
||||
it('leave a module whose cfg alternatives name a file each, or whose path a cfg_attr sets, unread', () => {
|
||||
expect(calls('crates/core/src/platform.rs', 'via_imp')).toEqual([]);
|
||||
expect(calls('crates/core/src/platform.rs', 'via_documented')).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Regression: #[path]-included test modules with `use super::*` must resolve
|
||||
* bare callees through the declaring parent module, not a same-named symbol
|
||||
* elsewhere in the workspace (ContextualWisdomLab/fast-mlsirm#1837). Case
|
||||
* from PR #1892 by @seonghobae.
|
||||
*/
|
||||
import { afterEach, beforeEach, expect, it } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
let root: string;
|
||||
let cg: CodeGraph | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-path-super-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cg?.close();
|
||||
cg = undefined;
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function indexWorkspace(layout: Record<string, string>) {
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'Cargo.toml'),
|
||||
'[workspace]\nmembers = ["crates/core", "crates/py"]\nresolver = "2"\n'
|
||||
);
|
||||
fs.mkdirSync(path.join(root, 'crates/core/src'), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, 'crates/py/src'), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, 'tests/unit'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'crates/core/Cargo.toml'),
|
||||
'[package]\nname = "core-crate"\nversion = "0.1.0"\nedition = "2021"\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'crates/py/Cargo.toml'),
|
||||
'[package]\nname = "py-crate"\nversion = "0.1.0"\nedition = "2021"\n'
|
||||
);
|
||||
for (const [file, text] of Object.entries(layout)) {
|
||||
const dest = path.join(root, file);
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, text);
|
||||
}
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
}
|
||||
|
||||
function callTargets(testFn: string): string[] {
|
||||
const caller = cg!
|
||||
.getNodesByKind('function')
|
||||
.find((n) => n.filePath === 'tests/unit/scaling_tests.rs' && n.name === testFn);
|
||||
expect(caller).toBeDefined();
|
||||
return cg!
|
||||
.getOutgoingEdges(caller!.id)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => {
|
||||
const target = cg!.getNode(e.target)!;
|
||||
return `${target.filePath}:${target.qualifiedName ?? target.name}`;
|
||||
});
|
||||
}
|
||||
|
||||
it('resolves use super::* callee through #[path] parent, not a same-named decoy (#1837)', async () => {
|
||||
await indexWorkspace({
|
||||
'crates/core/src/lib.rs': 'pub mod scaling;',
|
||||
'crates/core/src/scaling.rs': `pub fn predict_rating_multi(x: i32) -> i32 { x + 1 }
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../../tests/unit/scaling_tests.rs"]
|
||||
mod tests;
|
||||
`,
|
||||
'crates/py/src/lib.rs': 'pub fn predict_rating_multi(x: i32) -> i32 { x + 99 }',
|
||||
'tests/unit/scaling_tests.rs': `use super::*;
|
||||
|
||||
#[test]
|
||||
fn pr_elom_rowmean() {
|
||||
let _ = predict_rating_multi(1);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(callTargets('pr_elom_rowmean')).toEqual([
|
||||
'crates/core/src/scaling.rs:predict_rating_multi',
|
||||
]);
|
||||
});
|
||||
@@ -159,22 +159,25 @@ export function parsePackageName(cargoToml: string): string | null {
|
||||
|
||||
/**
|
||||
* The target roots a manifest names with a `path` of their own: `[lib] path =
|
||||
* "…"` and each `[[bin]] path = "…"` (ripgrep's `crates/core/main.rs`), as
|
||||
* written — relative to the manifest's directory.
|
||||
* "…"`, each `[[bin]] path = "…"` (ripgrep's `crates/core/main.rs`), and each
|
||||
* `[[example]]`, `[[test]]` and `[[bench]]` one (dioxus's
|
||||
* `examples/07-fullstack/handling_errors.rs`), as written — relative to the
|
||||
* manifest's directory.
|
||||
*/
|
||||
export function parseTargetPaths(cargoToml: string): { lib?: string; bins: string[] } {
|
||||
const out: { lib?: string; bins: string[] } = { bins: [] };
|
||||
let table: 'lib' | 'bin' | null = null;
|
||||
export function parseTargetPaths(cargoToml: string): { lib?: string; bins: string[]; others: string[] } {
|
||||
const out: { lib?: string; bins: string[]; others: string[] } = { bins: [], others: [] };
|
||||
let table: 'lib' | 'bin' | 'other' | null = null;
|
||||
for (const raw of cargoToml.split(/\r?\n/)) {
|
||||
const line = raw.replace(/#.*$/, '').trim();
|
||||
if (line.startsWith('[')) {
|
||||
table = line === '[lib]' ? 'lib' : line === '[[bin]]' ? 'bin' : null;
|
||||
table = line === '[lib]' ? 'lib' : line === '[[bin]]' ? 'bin' : /^\[\[(?:example|test|bench)\]\]$/.test(line) ? 'other' : null;
|
||||
continue;
|
||||
}
|
||||
const at = table && /^path\s*=\s*["']([^"'\n]+)["']/.exec(line)?.[1]?.replace(/\\/g, '/');
|
||||
if (!at) continue;
|
||||
if (table === 'lib') out.lib = at;
|
||||
else out.bins.push(at);
|
||||
else if (table === 'bin') out.bins.push(at);
|
||||
else out.others.push(at);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
+305
-20
@@ -2861,6 +2861,24 @@ function rustModuleDir(filePath: string): string {
|
||||
return path.posix.join(dir, base.replace(/\.rs$/, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a Rust file is inside the module whose file is `ownerFile` and whose
|
||||
* submodules live under `ownerDir`: below that directory, or loaded by a
|
||||
* `#[path]` declaration written inside it (fast-mlsirm's
|
||||
* `tests/unit/scaling_tests.rs` is `scaling.rs`'s `mod tests`, and sees its
|
||||
* private items).
|
||||
*/
|
||||
function rustFileUnder(file: string, ownerDir: string, ownerFile: string, context: ResolutionContext): boolean {
|
||||
let f = file;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
if (f.startsWith(`${ownerDir}/`) || (i > 0 && f === ownerFile)) return true;
|
||||
const claim = rustPathClaim(f, context);
|
||||
if (!claim) return false;
|
||||
f = claim.file;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard-library package names (and kubernetes' aliases of a few of its own
|
||||
* util packages). A dotted Go reference through one is taken for a call into
|
||||
@@ -3957,8 +3975,7 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex
|
||||
// trait's, reached through a value whose type the call's receiver
|
||||
// declares (matchRustTypedReceiverCall). A guess by its name alone stays out.
|
||||
if (candidate.kind === 'method' && RUST_TYPED_REFS.get(context)?.has(ref) && rustTraitOf(candidate, context)?.visibility !== 'private') return true;
|
||||
const owner = rustModuleDir(candidate.filePath);
|
||||
return ref.filePath.startsWith(owner + '/');
|
||||
return rustFileUnder(ref.filePath, rustModuleDir(candidate.filePath), candidate.filePath, context);
|
||||
}
|
||||
// A Dart `_name` is its library's, which spans the library file and its
|
||||
// parts: riverpod_generator's `class Counter extends _$Counter` names the
|
||||
@@ -8732,6 +8749,10 @@ interface RustUses {
|
||||
blocks: RustModBlock[];
|
||||
/** Modules it declares with a `#[path = "…"]` of their own (often one per cfg), whose file can't be told from their name. */
|
||||
pathMods: Set<string>;
|
||||
/** Modules it declares out of line with no `#[path]` (`mod name;`): the files the regular rules name. */
|
||||
mods: Set<string>;
|
||||
/** Whether it starts a binary of its own: a top-level `fn main` or `#![no_main]` (a trybuild case, a fuzz target). */
|
||||
main: boolean;
|
||||
/** Whether every brace closes, so `blocks` can be trusted to say what is inside them. */
|
||||
balanced: boolean;
|
||||
/** The offset each line starts at. */
|
||||
@@ -8852,6 +8873,8 @@ interface RustCrate {
|
||||
lib?: string;
|
||||
/** The binaries' roots its manifest gives a `path` (ripgrep's `crates/core/main.rs`), as project files. */
|
||||
bins: string[];
|
||||
/** The examples', tests' and benches' roots its manifest gives a `path`, as project files: each its own crate. */
|
||||
others: string[];
|
||||
}
|
||||
|
||||
interface RustCrateGraph {
|
||||
@@ -8892,8 +8915,8 @@ function rustCrateGraph(context: ResolutionContext): RustCrateGraph {
|
||||
if (!name) return false;
|
||||
const targets = parseTargetPaths(text!);
|
||||
const lib = targets.lib === undefined ? null : join(dir, targets.lib);
|
||||
const bins = targets.bins.map((bin) => join(dir, bin)).filter((bin): bin is string => bin !== null);
|
||||
crates.set(dir, { name, keys: new Map(), deps: new Set(), lib: lib ?? undefined, bins });
|
||||
const files = (rels: string[]): string[] => rels.map((rel) => join(dir, rel)).filter((file): file is string => file !== null);
|
||||
crates.set(dir, { name, keys: new Map(), deps: new Set(), lib: lib ?? undefined, bins: files(targets.bins), others: files(targets.others) });
|
||||
manifests.set(dir, text!);
|
||||
if (!byName.has(name)) byName.set(name, dir);
|
||||
return true;
|
||||
@@ -9000,7 +9023,7 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
|
||||
if (hit) return hit;
|
||||
const uses: RustUses = {
|
||||
names: new Set(), globs: new Set(), external: new Set(), bound: new Set(), boundFrom: new Map(),
|
||||
paths: new Map(), stdPaths: new Map(), globPaths: [], blocks: [], pathMods: new Set(), balanced: true, lineStarts: [],
|
||||
paths: new Map(), stdPaths: new Map(), globPaths: [], blocks: [], pathMods: new Set(), mods: new Set(), main: false, balanced: true, lineStarts: [],
|
||||
};
|
||||
const leaves = (tree: string): string[] => [
|
||||
...[...tree.matchAll(/([A-Za-z_]\w*)\s*(?=[,}]|$|\s+as\b)|\bas\s+([A-Za-z_]\w*)/g)]
|
||||
@@ -9067,9 +9090,16 @@ function rustUsesOf(filePath: string, context: ResolutionContext): RustUses {
|
||||
for (const g of tree.matchAll(/(\w+)\s*::\s*(?:\{[^}]*)?\*/g)) uses.globs.add(g[1]!);
|
||||
});
|
||||
// `#[path = "atomic_u64_native.rs"] mod imp;`, `#[cfg_attr(docsrs, path = "…")] pub mod de;`
|
||||
const pathModAt = new Set<number>();
|
||||
for (const m of text.matchAll(/#\[\s*(?:cfg_attr\s*\([^\]]*\bpath|path)\s*=[^\]]*\]\s*(?:#!?\[[^\]]*\]\s*)*(?:pub(?:\([^)]*\))?\s+)?mod\s+(?:r#)?([A-Za-z_]\w*)/g)) {
|
||||
uses.pathMods.add(m[1]!);
|
||||
pathModAt.add(m.index! + m[0].search(/\bmod\s+(?:r#)?[A-Za-z_]\w*$/));
|
||||
}
|
||||
// `mod name;` with no `#[path]` of its own.
|
||||
for (const m of text.matchAll(/\bmod\s+(?:r#)?([A-Za-z_]\w*)\s*;/g)) {
|
||||
if (!pathModAt.has(m.index!)) uses.mods.add(m[1]!);
|
||||
}
|
||||
uses.main = /^(?:pub\s+)?(?:async\s+)?fn\s+main\s*\(|^[ \t]*#!\[\s*no_main\s*\]/m.test(text);
|
||||
for (let at = 0; at !== -1; at = text.indexOf('\n', at) + 1 || -1) uses.lineStarts.push(at);
|
||||
memo.set(filePath, uses);
|
||||
return uses;
|
||||
@@ -9283,13 +9313,239 @@ function isRustTargetRoot(filePath: string, context: ResolutionContext): boolean
|
||||
const dir = rustCrateDirOf(filePath, graph);
|
||||
if (dir === null) return /(?:^|\/)(?:lib|main)\.rs$/.test(filePath);
|
||||
const crate = graph.crates.get(dir);
|
||||
if (crate && (crate.lib === filePath || crate.bins.includes(filePath))) return true;
|
||||
if (crate && (crate.lib === filePath || crate.bins.includes(filePath) || crate.others.includes(filePath))) return true;
|
||||
const rel = dir === '' ? filePath : filePath.slice(dir.length + 1);
|
||||
return /^(?:src\/(?:lib|main)\.rs|src\/bin\/[^/]+(?:\.rs|\/main\.rs)|(?:tests|examples|benches)\/[^/]+(?:\.rs|\/main\.rs)|build\.rs)$/.test(rel);
|
||||
}
|
||||
|
||||
/** A `#[path = "…"] mod name;` declaration. */
|
||||
interface RustPathDecl {
|
||||
/** The file it is written in, and the offset of its `mod`: the module it is declared in. */
|
||||
file: string;
|
||||
at: number;
|
||||
name: string;
|
||||
/**
|
||||
* The file it loads, from the project's root, read as the Rust Reference
|
||||
* reads the attribute; null for one a `cfg_attr` sets, which the build's
|
||||
* cfg decides, or one that leaves the project.
|
||||
*/
|
||||
target: string | null;
|
||||
}
|
||||
|
||||
const RUST_PATH_DECLS = new WeakMap<ResolutionContext, Map<string, readonly RustPathDecl[]>>();
|
||||
const RUST_PATH_LOADS = new WeakMap<ResolutionContext, Map<string, RustPathDecl[]>>();
|
||||
const RUST_PATH_LOADERS = new WeakMap<ResolutionContext, Map<string, readonly RustPathDecl[]>>();
|
||||
const RUST_FILES_BY_DIR = new WeakMap<ResolutionContext, Map<string, string[]>>();
|
||||
const NO_RUST_PATH_DECLS: readonly RustPathDecl[] = [];
|
||||
/** An out-of-line module declaration, `pub(crate) mod name;`, from its visibility. */
|
||||
const RUST_MOD_DECL = /(?:\bpub(?:\s*\([^()]*\))?\s+)?\bmod\s+(?:r#)?([A-Za-z_]\w*)\s*;/g;
|
||||
|
||||
/**
|
||||
* A file's own `#[path = "…"] mod name;` declarations. The attribute's path
|
||||
* is relative to the directory of the file it is written in — or, inside an
|
||||
* inline `mod`, to that module's own directory: the file's for a `mod.rs` or
|
||||
* a crate root, else the directory named after the file, then the inline
|
||||
* modules' names (the Rust Reference, "The path attribute").
|
||||
*/
|
||||
function rustPathDeclsIn(file: string, context: ResolutionContext): readonly RustPathDecl[] {
|
||||
let memo = RUST_PATH_DECLS.get(context);
|
||||
if (!memo) RUST_PATH_DECLS.set(context, (memo = new Map()));
|
||||
let decls = memo.get(file);
|
||||
if (!decls) memo.set(file, (decls = rustReadPathDecls(file, context)));
|
||||
return decls;
|
||||
}
|
||||
|
||||
function rustReadPathDecls(file: string, context: ResolutionContext): readonly RustPathDecl[] {
|
||||
let decls: RustPathDecl[] | undefined;
|
||||
// Both forms write `path =`: `#[path = "…"]`, `#[cfg_attr(…, path = "…")]`.
|
||||
// The bytes are checked first, so a file without the word is never decoded.
|
||||
if (!file.endsWith('.rs') || (context.fileContains && !context.fileContains(file, 'path'))) return NO_RUST_PATH_DECLS;
|
||||
const src = context.readFile(file);
|
||||
if (!src || !/\bpath\s*=/.test(src)) return NO_RUST_PATH_DECLS;
|
||||
const code = blankRustCode(src);
|
||||
let blocks: RustModBlock[] | undefined;
|
||||
for (const m of code.matchAll(RUST_MOD_DECL)) {
|
||||
let target: string | null | undefined;
|
||||
for (const [from, to] of rustAttributesBefore(code, m.index!)) {
|
||||
const raw = src.slice(from, to);
|
||||
const plain = /^#\s*\[\s*path\s*=\s*"([^"]*)"\s*\]$/.exec(raw);
|
||||
if (plain) target = plain[1]!.replace(/\\/g, '/');
|
||||
else if (target === undefined && /^#\s*\[\s*cfg_attr\b[\s\S]*\bpath\s*=/.test(raw)) target = null;
|
||||
}
|
||||
if (target === undefined) continue;
|
||||
// The `mod` keyword's offset (`mod modules;` holds a second `mod`).
|
||||
const at = m.index! + /\bmod\s/.exec(m[0])!.index;
|
||||
// At the file's top level it is the file's own; only one inside braces
|
||||
// (an inline module's, or a macro's) needs the file's block layout.
|
||||
const block = rustBraceDepth(code, at) === 0 ? null : rustBlockAt((blocks ??= rustUsesOf(file, context).blocks), at);
|
||||
// From the file's directory; in an inline module, from that module's.
|
||||
let base = rustParentDir(file);
|
||||
if (block) {
|
||||
base = isRustTargetRoot(file, context) || /(?:^|\/)mod\.rs$/.test(file) ? base : file.replace(/\.rs$/, '');
|
||||
const names: string[] = [];
|
||||
for (let b: RustModBlock | null = block; b; b = b.parent) names.unshift(b.name);
|
||||
for (const name of names) base = rustJoin(base, name);
|
||||
}
|
||||
let loads: string | null = null;
|
||||
if (target !== null && !/^(?:\/|[A-Za-z]:)/.test(target)) {
|
||||
const joined = path.posix.normalize(rustJoin(base, target));
|
||||
loads = escapesRoot(joined) ? null : joined;
|
||||
}
|
||||
(decls ??= []).push({ file, at, name: m[1]!, target: loads });
|
||||
}
|
||||
return decls ?? NO_RUST_PATH_DECLS;
|
||||
}
|
||||
|
||||
/** How many braces are open at `at` in blanked code. */
|
||||
function rustBraceDepth(code: string, at: number): number {
|
||||
let depth = 0;
|
||||
for (let i = 0; i < at; i++) {
|
||||
const c = code.charCodeAt(i);
|
||||
if (c === 123) depth++;
|
||||
else if (c === 125 && depth > 0) depth--;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
/** Each file a `#[path]` of the project loads → the declarations that load it: every Rust file's, read once. */
|
||||
function rustPathLoads(context: ResolutionContext): Map<string, RustPathDecl[]> {
|
||||
const hit = RUST_PATH_LOADS.get(context);
|
||||
if (hit) return hit;
|
||||
const out = new Map<string, RustPathDecl[]>();
|
||||
RUST_PATH_LOADS.set(context, out);
|
||||
for (const file of context.getAllFiles()) {
|
||||
for (const decl of rustPathDeclsIn(file, context)) {
|
||||
if (decl.target === null) continue;
|
||||
const list = out.get(decl.target);
|
||||
if (list) list.push(decl);
|
||||
else out.set(decl.target, [decl]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The project's Rust files directly in `dir`. */
|
||||
function rustFilesIn(dir: string, context: ResolutionContext): readonly string[] {
|
||||
let index = RUST_FILES_BY_DIR.get(context);
|
||||
if (!index) {
|
||||
index = new Map();
|
||||
for (const file of context.getAllFiles()) {
|
||||
if (!file.endsWith('.rs')) continue;
|
||||
const parent = rustParentDir(file);
|
||||
const list = index.get(parent);
|
||||
if (list) list.push(file);
|
||||
else index.set(parent, [file]);
|
||||
}
|
||||
RUST_FILES_BY_DIR.set(context, index);
|
||||
}
|
||||
return index.get(dir) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the regular rules place a Rust file: a crate root (one with a
|
||||
* `fn main` of its own too, as a trybuild case is), or `name.rs` /
|
||||
* `name/mod.rs` that a plain `mod name;` declares in the module owning its
|
||||
* directory (that directory's `mod.rs`, the file named after it, or a crate
|
||||
* root in it) — or in an inline module of the module owning a directory
|
||||
* above it, whose names make up the rest of the path (dioxus's
|
||||
* ecommerce-site: `mod components { pub mod home; }` in `src/main.rs` places
|
||||
* `src/components/home.rs`). Such a file is that module, whatever else loads it.
|
||||
*/
|
||||
function rustPlacedByName(file: string, context: ResolutionContext): boolean {
|
||||
if (isRustTargetRoot(file, context) || rustUsesOf(file, context).main) return true;
|
||||
const dir = rustParentDir(file);
|
||||
const isMod = file === 'mod.rs' || file.endsWith('/mod.rs');
|
||||
if (isMod && dir === '') return false;
|
||||
const name = path.posix.basename(isMod ? dir : file, '.rs');
|
||||
let parent = isMod ? rustParentDir(dir) : dir;
|
||||
// The inline modules, outermost first, between the owner's directory and the file's.
|
||||
const inline: string[] = [];
|
||||
for (let depth = 0; depth < 4; depth++) {
|
||||
const owners = [rustJoin(parent, 'mod.rs'), ...(parent === '' ? [] : [`${parent}.rs`])];
|
||||
for (const f of rustFilesIn(parent, context)) if (isRustTargetRoot(f, context)) owners.push(f);
|
||||
for (const owner of owners) {
|
||||
if (owner === file || !context.fileExists(owner)) continue;
|
||||
const uses = rustUsesOf(owner, context);
|
||||
if (uses.mods.has(name) && (inline.length === 0 || rustHasInlineModules(uses.blocks, inline))) return true;
|
||||
}
|
||||
if (parent === '') break;
|
||||
inline.unshift(path.posix.basename(parent));
|
||||
parent = rustParentDir(parent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether a file declares the inline modules `names` (outermost first) nested in one another from its top level. */
|
||||
function rustHasInlineModules(blocks: readonly RustModBlock[], names: readonly string[]): boolean {
|
||||
return blocks.some((block) => {
|
||||
let b: RustModBlock | null = block;
|
||||
for (let i = names.length - 1; i >= 0; i--) {
|
||||
if (!b || b.name !== names[i]) return false;
|
||||
b = b.parent;
|
||||
}
|
||||
return b === null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The `#[path]` declarations that load `file`, for a file the regular rules
|
||||
* don't place (rustPlacedByName); none for one they do. Only then is every
|
||||
* Rust file of the project read for its declarations.
|
||||
*/
|
||||
function rustPathLoaders(file: string, context: ResolutionContext): readonly RustPathDecl[] {
|
||||
let memo = RUST_PATH_LOADERS.get(context);
|
||||
if (!memo) RUST_PATH_LOADERS.set(context, (memo = new Map()));
|
||||
const hit = memo.get(file);
|
||||
if (hit) return hit;
|
||||
const loaders = !file.endsWith('.rs') || rustPlacedByName(file, context)
|
||||
? NO_RUST_PATH_DECLS
|
||||
: rustPathLoads(context).get(file) ?? NO_RUST_PATH_DECLS;
|
||||
memo.set(file, loaders);
|
||||
return loaders;
|
||||
}
|
||||
|
||||
/** The spans of the outer attributes (`#[…]`) written right before `at` in blanked code, nearest first. */
|
||||
function rustAttributesBefore(code: string, at: number): Array<[number, number]> {
|
||||
const spans: Array<[number, number]> = [];
|
||||
let p = at;
|
||||
for (;;) {
|
||||
let q = p - 1;
|
||||
while (q >= 0 && /\s/.test(code[q]!)) q--;
|
||||
if (code[q] !== ']') break;
|
||||
let depth = 0;
|
||||
let open = -1;
|
||||
for (let i = q; i >= 0; i--) {
|
||||
if (code[i] === ']') depth++;
|
||||
else if (code[i] === '[' && --depth === 0) {
|
||||
open = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let hash = open - 1;
|
||||
while (hash >= 0 && /\s/.test(code[hash]!)) hash--;
|
||||
if (open < 0 || code[hash] !== '#') break;
|
||||
spans.push([hash, q + 1]);
|
||||
p = hash;
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
/**
|
||||
* The one `#[path]` declaration that loads `file` as a module, or null: a file
|
||||
* the regular rules place, one no such declaration loads, or one several do
|
||||
* (tests that each load a shared helper; then which module it is depends on
|
||||
* the crate being built).
|
||||
*/
|
||||
function rustPathClaim(file: string, context: ResolutionContext): RustPathDecl | null {
|
||||
const loaders = rustPathLoaders(file, context);
|
||||
return loaders.length === 1 ? loaders[0]! : null;
|
||||
}
|
||||
|
||||
/** The module a Rust file is, at its own top level. */
|
||||
function rustFileModule(filePath: string, context: ResolutionContext): RustModule {
|
||||
// A file a `#[path]` loads owns its directory, as a `mod.rs` does: rustc
|
||||
// reads every `#[path]` file so.
|
||||
if (rustPathClaim(filePath, context)) return { file: filePath, dir: rustParentDir(filePath), block: null, root: false };
|
||||
const root = isRustTargetRoot(filePath, context);
|
||||
const dir = root || filePath.endsWith('/mod.rs') || filePath === 'mod.rs' ? rustParentDir(filePath) : filePath.replace(/\.rs$/, '');
|
||||
return { file: filePath, dir, block: null, root };
|
||||
@@ -9314,10 +9570,15 @@ function rustModuleAt(filePath: string, offset: number, context: ResolutionConte
|
||||
* `main.rs` above it. null when no single root is the file's: a
|
||||
* `tests/support/` module serves every test that declares it.
|
||||
*/
|
||||
function rustCrateRootModule(filePath: string, context: ResolutionContext): RustModule | null {
|
||||
function rustCrateRootModule(filePath: string, context: ResolutionContext, depth = 0): RustModule | null {
|
||||
// A file a `#[path]` loads is in the crate of the file that declares it.
|
||||
const claim = depth < 8 ? rustPathClaim(filePath, context) : null;
|
||||
if (claim) return rustCrateRootModule(claim.file, context, depth + 1);
|
||||
const graph = rustCrateGraph(context);
|
||||
const dir = rustCrateDirOf(filePath, graph);
|
||||
if (dir === null) return rustNearestRootModule(filePath, context);
|
||||
// An example, test or bench the manifest places with a path of its own is its own crate's root.
|
||||
if (graph.crates.get(dir)?.others.includes(filePath)) return { file: filePath, dir: rustParentDir(filePath), block: null, root: true };
|
||||
const base = dir === '' ? '' : `${dir}/`;
|
||||
const rel = filePath.slice(base.length);
|
||||
const target = /^(src\/bin|tests|examples|benches)\/([^/]+?)(\.rs$|\/)/.exec(rel);
|
||||
@@ -9370,14 +9631,21 @@ function rustLibModule(crateDir: string, context: ResolutionContext): RustModule
|
||||
/**
|
||||
* The module `name` that `m` declares: inline (`mod name { … }`), else
|
||||
* `<name>.rs` or `<name>/mod.rs` in its directory, else one it passes on under
|
||||
* that name — dioxus's `pub use dioxus_hooks as hooks;`. null for one a
|
||||
* `#[path]` puts elsewhere — tokio's `process::imp` is `unix/mod.rs` or
|
||||
* `windows.rs` by cfg — as for one with no file.
|
||||
* that name — dioxus's `pub use dioxus_hooks as hooks;`. One a `#[path]`
|
||||
* puts elsewhere is the file it names when `m` declares it once, whatever
|
||||
* else loads that file too (a helper several tests load is each test's);
|
||||
* null when cfg alternatives name a file each (tokio's `process::imp` is
|
||||
* `unix/mod.rs` or `windows.rs`) or a `cfg_attr` sets the path, as for one
|
||||
* with no file.
|
||||
*/
|
||||
function rustSubmodule(m: RustModule, name: string, context: ResolutionContext, depth = 0): RustModule | null {
|
||||
const dir = rustJoin(m.dir, name);
|
||||
const layout = rustUsesOf(m.file, context);
|
||||
if (layout.pathMods.has(name)) return null;
|
||||
if (layout.pathMods.has(name)) {
|
||||
const decls = rustPathDeclsIn(m.file, context).filter((d) => d.name === name && rustBlockAt(layout.blocks, d.at) === m.block);
|
||||
const target = decls.length === 1 ? decls[0]!.target : null;
|
||||
return target !== null && context.fileExists(target) ? { file: target, dir: rustParentDir(target), block: null, root: false } : null;
|
||||
}
|
||||
const block = layout.blocks.find((b) => b.name === name && b.parent === m.block);
|
||||
if (block) return { file: m.file, dir, block, root: false };
|
||||
for (const file of [`${dir}.rs`, `${dir}/mod.rs`]) {
|
||||
@@ -9392,11 +9660,18 @@ function rustSubmodule(m: RustModule, name: string, context: ResolutionContext,
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The module `m` is declared in — what `super::` names — or null above a crate's root. */
|
||||
/**
|
||||
* The module `m` is declared in — what `super::` names — or null above a
|
||||
* crate's root. A file a `#[path]` loads is declared where that attribute is,
|
||||
* wherever the file lies (fast-mlsirm's `tests/unit/scaling_tests.rs` is
|
||||
* `crates/core/src/scaling.rs`'s `mod tests`).
|
||||
*/
|
||||
function rustSuperModule(m: RustModule, context: ResolutionContext): RustModule | null {
|
||||
if (m.block) {
|
||||
return m.block.parent ? { file: m.file, dir: rustParentDir(m.dir), block: m.block.parent, root: false } : rustFileModule(m.file, context);
|
||||
}
|
||||
const claim = rustPathClaim(m.file, context);
|
||||
if (claim) return rustModuleAt(claim.file, claim.at, context);
|
||||
if (m.root) return null;
|
||||
const dir = rustParentDir(m.dir);
|
||||
for (const file of [rustJoin(dir, 'mod.rs'), ...(dir === '' ? [] : [`${dir}.rs`]), rustJoin(dir, 'lib.rs'), rustJoin(dir, 'main.rs')]) {
|
||||
@@ -9523,7 +9798,7 @@ function rustGlobbedItems(
|
||||
const from = fromOf(g);
|
||||
const mod = rustWalkModules(g.segs, from, context);
|
||||
if (mod === 'outside') continue;
|
||||
const items = mod === null ? null : rustModuleItems(mod, name, context, seen, !rustModuleWithin(from, mod));
|
||||
const items = mod === null ? null : rustModuleItems(mod, name, context, seen, !rustModuleWithin(from, mod, context));
|
||||
if (items === null) unknown = true;
|
||||
else found.push(...items);
|
||||
}
|
||||
@@ -9531,14 +9806,17 @@ function rustGlobbedItems(
|
||||
}
|
||||
|
||||
/** Whether module `inner` is `outer` or one declared inside it, inline or in a file below its directory. */
|
||||
function rustModuleWithin(inner: RustModule, outer: RustModule): boolean {
|
||||
function rustModuleWithin(inner: RustModule, outer: RustModule, context?: ResolutionContext, depth = 0): boolean {
|
||||
if (inner.file === outer.file) {
|
||||
for (let b: RustModBlock | null = inner.block; ; b = b.parent) {
|
||||
if (b === outer.block) return true;
|
||||
if (b === null) return false;
|
||||
}
|
||||
}
|
||||
return inner.file.startsWith(outer.dir === '' ? '' : `${outer.dir}/`) && !(outer.dir === '' && outer.block !== null);
|
||||
if (inner.file.startsWith(outer.dir === '' ? '' : `${outer.dir}/`) && !(outer.dir === '' && outer.block !== null)) return true;
|
||||
// A file a `#[path]` loads is inside the module that declares it.
|
||||
const claim = context && depth < 8 && inner.block === null ? rustPathClaim(inner.file, context) : null;
|
||||
return claim !== null && rustModuleWithin(rustModuleAt(claim.file, claim.at, context!), outer, context, depth + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9716,9 +9994,9 @@ const RUST_MODULE_ITEM_KINDS = new Set(['function', 'struct', 'enum', 'union', '
|
||||
* 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 {
|
||||
function isRustItemVisibleFrom(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): 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)}/`);
|
||||
return candidate.filePath === ref.filePath || rustFileUnder(ref.filePath, rustModuleDir(candidate.filePath), candidate.filePath, context);
|
||||
}
|
||||
|
||||
/** What a Rust `impl` declares besides its functions: `type Err = …;`, `const MAX: usize = …;`. */
|
||||
@@ -9761,7 +10039,7 @@ function rustGlobCovers(uses: RustUses, candidate: Node, ref: UnresolvedRef): bo
|
||||
*/
|
||||
export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
if (RUST_SCOPED_REFS.get(context)?.has(ref)) return true;
|
||||
if (!isRustItemVisibleFrom(candidate, ref)) return false;
|
||||
if (!isRustItemVisibleFrom(candidate, ref, context)) 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.
|
||||
@@ -10088,7 +10366,10 @@ export function rustPathTarget(ref: UnresolvedRef, context: ResolutionContext):
|
||||
return ids === null ? null : [...ids].map((id) => context.getNodeById?.(id)).filter((n): n is Node => !!n);
|
||||
}
|
||||
let items = rustUsePathItems(path, from, context, new Set());
|
||||
if (items === null && bare) {
|
||||
// As a 2015 path, from the crate's root — unless the module declares the
|
||||
// head itself, where a `#[path]` puts it (a cfg's choice): another module
|
||||
// of that name at the root is not it.
|
||||
if (items === null && bare && !rustUsesOf(from.file, context).pathMods.has(head)) {
|
||||
const root = rustCrateRootModule(ref.filePath, context);
|
||||
if (root) items = rustUsePathItems(path, root, context, new Set());
|
||||
}
|
||||
@@ -11127,7 +11408,7 @@ function rustTypeLookup(name: string, writtenPath: string[], file: string, conte
|
||||
if (segments.length === 0 && (RUST_STD_TYPES.has(real) || RUST_PRELUDE.has(real))) return outside;
|
||||
return { nodes: [], outside: false, undeclared: true, name: real };
|
||||
}
|
||||
let nodes = all.filter((n) => isRustItemVisibleFrom(n, fileRef));
|
||||
let nodes = all.filter((n) => isRustItemVisibleFrom(n, fileRef, context));
|
||||
// A path the project's modules can be walked along, written or the one a
|
||||
// `use` binds, names the item of the module it walks to, as a `use` does in
|
||||
// isRustNameInScope: tokio's `crate::runtime::Handle` is the one
|
||||
@@ -20643,6 +20924,10 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
|
||||
RUST_VISIBLE_ITEMS.delete(context);
|
||||
RUST_PATH_TARGETS.delete(context);
|
||||
RUST_TYPE_MEMBERS.delete(context);
|
||||
RUST_PATH_DECLS.delete(context);
|
||||
RUST_PATH_LOADS.delete(context);
|
||||
RUST_PATH_LOADERS.delete(context);
|
||||
RUST_FILES_BY_DIR.delete(context);
|
||||
RUST_SCOPED_REFS.delete(context);
|
||||
RUST_GLOB_ITEMS.delete(context);
|
||||
RUST_CRATES.delete(context);
|
||||
|
||||
Reference in New Issue
Block a user