Skip to content
Open
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 @@ -192,6 +192,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 call through a type that a path names, like `crate::a::Cache::open()`, now links only to that type's own function. Before, it could land on the function of a type of the same name in another module of the same file, on one a function declares inside its own body, or on an `impl` written for a reference to the type like `impl Open for &Cache`, and a path through a name that a `use` inside a function binds, like `use crate::b as a;`, or through a module declared inside a function could land on a module of that name elsewhere in the file. Re-index Rust projects after upgrading.
- 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. A name only one of a module's `cfg` alternatives holds now links through it, like `crate::process::unix::GlobalOrphanQueue` through tokio's `#[path]` modules for Unix and Windows. 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.
Expand Down
285 changes: 285 additions & 0 deletions __tests__/rust-path-call-edge-cases.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
/**
* A Rust call through a type a path names — `crate::a::Cache::open()` — is to
* the associated function of the type that path walks to, and no other's:
* - not a namesake type's, in another inline module of the same file or one a
* function declares in its own body;
* - not an `impl` for a reference to the type (`impl Open for &Cache`), which
* is the reference type's.
* A path whose head a `use` in the function's body binds (`use crate::b as
* a;`) is the path that `use` names, not the module `a` beside it; a `mod a {
* … }` the function declares is no module of the file, so neither its call
* nor a later `pub mod a` takes the other's items.
*/
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-edge-'));
const files: Record<string, string> = {
'Cargo.toml': '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2021"\n',
'src/lib.rs': 'pub mod inline;\npub mod local;\npub mod shadow;\npub mod order;\npub mod refimpl;\npub mod traits;\npub mod shapes;\npub mod store;\npub mod split;\n',
// a's Cache has only a trait's provided `open`; b's has an inherent one.
'src/inline.rs': `pub mod a {
pub struct Cache;
pub trait Open {
fn open() -> usize {
1
}
}
impl Open for Cache {}
}

pub mod b {
pub struct Cache;
impl Cache {
pub fn open() -> usize {
2
}
}
}

pub fn through_a() -> usize {
crate::inline::a::Cache::open()
}

pub fn through_b() -> usize {
crate::inline::b::Cache::open()
}
`,
// The root's Cache has only the trait's provided `open`; a function
// declares a Cache of its own, whose inherent `open` is not the root's.
'src/local.rs': `pub struct Cache;

pub trait Open {
fn open() -> usize {
1
}
}

impl Open for Cache {}

pub fn build_local() -> usize {
struct Cache;
impl Cache {
pub fn open() -> usize {
2
}
}
0
}

pub fn through_root() -> usize {
crate::local::Cache::open()
}
`,
'src/shadow.rs': `pub mod a {
pub struct Cache;
impl Cache {
pub fn open() -> usize {
1
}
}
pub fn make() -> usize {
1
}
}

pub mod b {
pub struct Cache;
impl Cache {
pub fn open() -> usize {
2
}
}
pub fn make() -> usize {
2
}
}

pub fn through_module() -> usize {
a::Cache::open()
}

pub fn through_use() -> usize {
use crate::shadow::b as a;
a::Cache::open()
}

pub fn through_use_fn() -> usize {
use crate::shadow::b as a;
a::make()
}

pub fn through_local_mod() -> usize {
mod a {
pub use crate::shadow::b::Cache;
}
a::Cache::open()
}

pub fn through_local_mod_fn() -> usize {
mod a {
pub fn make() -> usize {
3
}
}
a::make()
}
`,
// b comes first in the file, and a function before `pub mod a` declares a
// `mod a` of its own: the module's own `a` is still the one after it.
'src/order.rs': `pub mod b {
pub struct Cache;
impl Cache {
pub fn open() -> usize {
2
}
}
}

pub fn decoy() {
mod a {
pub use crate::order::b::Cache;
}
}

pub mod a {
pub struct Cache;
impl Cache {
pub fn open() -> usize {
1
}
}
}

pub fn via_order() -> usize {
a::Cache::open()
}
`,
// Cache takes the trait's provided `open`; the impl for `&Cache` writes its own.
'src/refimpl.rs': `pub struct Cache;

pub trait Open {
fn open() -> usize {
1
}
}

impl Open for Cache {}

impl Open for &crate::refimpl::Cache {
fn open() -> usize {
3
}
}

pub fn through_cache() -> usize {
crate::refimpl::Cache::open()
}
`,
// A trait, and a struct of the same name in another module with an inherent method of the trait's method's name.
'src/traits.rs': `pub trait Region {
fn size() -> usize;
}
`,
'src/shapes.rs': `pub struct Region;

impl Region {
pub fn size() -> usize {
0
}
}

pub fn through_trait() -> usize {
crate::traits::Region::size()
}
`,
// An impl written in another file than its type: by path, and by a bare name a `use` brings in.
'src/store.rs': `pub struct Store;
pub struct Vault;
`,
'src/split.rs': `use crate::store::Vault;

impl crate::store::Store {
pub fn drop_all() -> usize {
1
}
}

impl Vault {
pub fn seal() -> usize {
2
}
}

pub fn through_path() -> usize {
crate::store::Store::drop_all()
}

pub fn through_use() -> usize {
crate::store::Vault::seal()
}
`,
};
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 });
});

/** What the function `name` in `file` calls, as `kind qualifiedName file:line`. */
const calls = (file: string, name: string): string[] => {
const fn = cg.getNodesInFile(file).find((n) => n.name === name && n.kind === 'function');
expect(fn, `${name} in ${file}`).toBeDefined();
return cg.getOutgoingEdgesFrom([fn!.id]).filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`).sort();
};

describe('Rust calls through a type a path names', () => {
it('never reach a namesake type\'s function in another inline module of the file', () => {
expect(calls('src/inline.rs', 'through_a')).toEqual([]);
expect(calls('src/inline.rs', 'through_b')).toEqual(['method Cache::open src/inline.rs:14']);
// Controls: an impl written in another file than its type is still the
// type's, by its path or by a name a `use` brings in; and a trait's own
// declaration is still reached through the trait, not a namesake struct's.
expect(calls('src/split.rs', 'through_path')).toEqual(['method Store::drop_all src/split.rs:4']);
expect(calls('src/split.rs', 'through_use')).toEqual(['method Vault::seal src/split.rs:10']);
expect(calls('src/shapes.rs', 'through_trait')).toEqual(['method Region::size src/traits.rs:2']);
});

it('never reach the function of a namesake type a function declares in its own body', () => {
expect(calls('src/local.rs', 'through_root')).toEqual([]);
});

it('never reach the function of an impl for a reference to the type', () => {
expect(calls('src/refimpl.rs', 'through_cache')).toEqual([]);
});

it('read a head a `use` in the function\'s body binds as the path that `use` names', () => {
expect(calls('src/shadow.rs', 'through_use')).toEqual(['method Cache::open src/shadow.rs:16']);
expect(calls('src/shadow.rs', 'through_use_fn')).toEqual(['function make src/shadow.rs:20']);
// Where no `use` in the body binds `a`, it is the module `a` (control).
expect(calls('src/shadow.rs', 'through_module')).toEqual(['method Cache::open src/shadow.rs:4']);
});

it('never reach module a\'s item through a module `a` the function declares in its own body', () => {
expect(calls('src/shadow.rs', 'through_local_mod')).not.toContain('method Cache::open src/shadow.rs:4');
expect(calls('src/shadow.rs', 'through_local_mod_fn')).not.toContain('function make src/shadow.rs:8');
});

it('keep the module\'s own `mod a` when a function before it declares a `mod a` of its own', () => {
expect(calls('src/order.rs', 'via_order')).toEqual(['method Cache::open src/order.rs:19']);
});
});
Loading