Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,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.
Expand Down
148 changes: 148 additions & 0 deletions __tests__/rust-path-attribute-modules.test.ts
Original file line number Diff line number Diff line change
@@ -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([]);
});
});
86 changes: 86 additions & 0 deletions __tests__/rust-path-super-glob.test.ts
Original file line number Diff line number Diff line change
@@ -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',
]);
});
17 changes: 10 additions & 7 deletions src/resolution/frameworks/cargo-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading