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 @@ -123,6 +123,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `codegraph_explore` and `codegraph context` now include some of the subtypes and implementors of a class or interface you ask about, and the type hierarchy the library returns for one (`getTypeHierarchy`) now lists all of them. Before, a bug in the hierarchy walk left every type without subtypes, so only what it extends or implements came back. Thanks @jocax for the report and @danusha2345 for the fix. (#2833)
- In Rust, code that uses an enum only through its variants, like `mode::Mode::A` in an expression, a `Mode::B =>` match arm or `Self::A` inside the enum's own `impl`, now shows up in that enum's callers and impact, linked to the enum that actually declares the variant rather than to a same-named type elsewhere. Standard-library variants like `Ordering::Less` or `Option::Some` and associated items like `Foo::new()` or `Foo::MAX` don't count as using a project enum. Re-index Rust projects after upgrading. Thanks @mg-mg-mg for the report and @danusha2345. (#2328)
- 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)
- 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 JavaScript and TypeScript, the functions written inside a named object literal are now symbols of their own even when the object isn't exported: a plain `const api = { load() {…} }`, an object declared inside an IIFE or a function, and a namespace hung on the page or on another object, like `window.App = { init() {…} }`, `App.utils = {…}` or `dw_page = {…}`. Calls such as `api.load()`, `window.App.init()`, `App.utils.pad()`, a sibling's `this.render()`, and `App.init()` from another script on the page now reach them, the calls made inside a member belong to that member instead of the object, and a bare `init()` is no longer taken for `App.init`, so script-tag apps no longer lose most of their code from `callers` and impact. A member's qualified name now carries its object, exported or not (`api::load`, `window.App::init`), so asking for `App.init` finds it. Re-index JavaScript and TypeScript projects after upgrading. Thanks @tkhoaaa for the report and @danusha2345. (#2300)
Expand Down
73 changes: 73 additions & 0 deletions __tests__/rust-crate-roots.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* The root a Rust path's `crate::` names, as the manifest says: a `[[bin]]`
* or `[lib]` with a `path` of its own (ripgrep's `crates/core/main.rs`), and
* a package whose `name` its manifest writes after an array (axum-core's
* `categories = [ … ]` first).
*/
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';

const roots: string[] = [];
const graphs: CodeGraph[] = [];

async function index(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-crate-roots-'));
roots.push(root);
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);
}
const cg = await CodeGraph.init(root, { index: true });
graphs.push(cg);
return cg;
}

let bin: CodeGraph;
let member: CodeGraph;

beforeAll(async () => {
bin = await index({
'Cargo.toml': '[package]\nname = "ripgrep"\nversion = "0.1.0"\nedition = "2021"\n\n[[bin]]\nbench = false\npath = "crates/core/main.rs"\nname = "rg"\n',
'crates/core/main.rs': 'mod flags;\nmod messages;\n\nfn main() {\n let _ = messages::errored();\n}\n',
'crates/core/messages.rs': 'pub fn errored() -> bool {\n false\n}\n',
'crates/core/flags/mod.rs': 'pub mod parse;\npub mod doc;\n\npub enum Category {\n Input,\n}\n',
'crates/core/flags/parse.rs': 'pub fn lookup() -> usize {\n 1\n}\n',
'crates/core/flags/doc.rs': 'use crate::flags::Category;\n\npub fn generate() -> usize {\n crate::flags::parse::lookup()\n}\n',
// Namesakes the paths must not reach.
'crates/core/flags/doc/parse.rs': 'pub fn lookup() -> usize {\n 2\n}\n',
'crates/core/other.rs': 'pub fn errored() -> bool {\n true\n}\npub enum Category {\n Output,\n}\n',
});
member = await index({
'Cargo.toml': '[workspace]\nmembers = ["core-lib"]\n',
'core-lib/Cargo.toml': '[package]\ncategories = ["asynchronous", "network-programming"]\ndescription = "Core types"\nedition = "2021"\nname = "core-lib"\nversion = "0.1.0"\n',
'core-lib/src/lib.rs': 'pub mod body;\npub mod other;\n',
'core-lib/src/body.rs': 'pub struct Body;\n\n#[cfg(test)]\nmod tests {\n use crate::body::Body;\n}\n',
'core-lib/src/other.rs': 'pub struct Body;\n',
});
});

afterAll(() => {
for (const cg of graphs) cg.close();
for (const root of roots) fs.rmSync(root, { recursive: true, force: true });
});

const describeEdges = (cg: CodeGraph, file: string, kind: string, name?: string): string[] => {
const ids = cg.getNodesInFile(file).filter((n) => name === undefined || n.name === name).map((n) => n.id);
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === kind)
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
};

describe('the crate a Rust path starts from', () => {
it('is the binary a manifest places with a path of its own', () => {
expect(describeEdges(bin, 'crates/core/flags/doc.rs', 'calls', 'generate')).toEqual(['function lookup crates/core/flags/parse.rs:1']);
expect(describeEdges(bin, 'crates/core/flags/doc.rs', 'imports')).toEqual(['enum Category crates/core/flags/mod.rs:4']);
expect(describeEdges(bin, 'crates/core/main.rs', 'calls', 'main')).toEqual(['function errored crates/core/messages.rs:1']);
});

it('is a package whose manifest writes its name after an array', () => {
expect(describeEdges(member, 'core-lib/src/body.rs', 'imports')).toEqual(['struct Body core-lib/src/body.rs:1']);
});
});
94 changes: 94 additions & 0 deletions __tests__/rust-module-path-call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,74 @@ mod twice;
mod inline_reexport;
mod fs;
mod uses_std;
mod b;
mod x;
mod types;
mod type_;
mod decoy_list;
mod uses_config;
mod walks;
mod plat;

pub struct Config;

pub fn root_fn() -> usize {
0
}

pub fn via_self() -> usize {
self::util::take(1)
}
`,
// `super::x` and `crate::x`, with no module between them and the item.
'src/b/mod.rs': 'pub mod c;\npub fn helper() -> usize {\n 1\n}\n',
'src/b/c.rs': 'pub fn helper() -> usize {\n 2\n}\npub fn via_bare_super() -> usize {\n super::helper()\n}\npub fn via_bare_crate() -> usize {\n crate::root_fn()\n}\n',
// `super` in an inline `mod tests { }` is the file's own module, not its directory's parent.
'src/x/mod.rs': 'pub mod tested;\npub mod shared;\npub struct Config;\npub fn root_fn() -> usize {\n 9\n}\n',
'src/x/shared.rs': 'pub fn helper() -> usize {\n 3\n}\n',
'src/x/tested.rs': `mod shared;

pub fn tested_helper() -> usize {
4
}

#[cfg(test)]
mod tests {
use super::tested_helper;

fn via_inline_super() -> usize {
super::shared::helper() + tested_helper()
}
}
`,
'src/x/tested/shared.rs': 'pub fn helper() -> usize {\n 5\n}\n',
// A module's item it passes on: a `pub use`, a `pub use …::*`.
'src/types/mod.rs': 'mod list;\npub use self::list::make_list;\n',
'src/types/list.rs': 'pub fn make_list() -> usize {\n 6\n}\n',
'src/decoy_list.rs': 'pub fn make_list() -> usize {\n 7\n}\n',
'src/type_/mod.rs': 'pub mod prelude;\npub use prelude::*;\n\npub struct Type;\n\nimpl Type {\n pub fn list() -> usize {\n 8\n }\n}\n',
'src/type_/prelude.rs': 'pub fn list() -> usize {\n 9\n}\n',
'src/uses_config.rs': 'use crate::{Config};\n',
// A re-export written in a macro's braces, as `cfg_if!` takes them, is the module's.
'src/plat/mod.rs': 'cfg_if::cfg_if! {\n if #[cfg(unix)] {\n mod unix;\n pub use self::unix::open;\n }\n}\n',
'src/plat/unix.rs': 'pub fn open() -> usize {\n 10\n}\n',
'src/decoy_open.rs': 'pub fn open() -> usize {\n 11\n}\n',
'src/walks.rs': `pub fn via_reexport() -> usize {
crate::types::make_list()
}

pub fn via_glob() -> usize {
crate::type_::list()
}

pub fn via_macro_reexport() -> usize {
crate::plat::open()
}

pub fn via_block_std() {
use std::fs;
let _ = fs::read("config.toml");
}
`,
// A namesake in an inline module, before the module's own item.
'src/twice.rs': `#[cfg(test)]
Expand Down Expand Up @@ -219,5 +283,35 @@ describe('Rust calls through a module path', () => {
it('leave a path through a module a `use` takes from the standard library to it', () => {
// `fs::read` under `use std::fs;` is std's, not `src/fs/mod.rs`'s `read`.
expect(callsFrom('src/uses_std.rs', 'loads')).toEqual([]);
expect(callsFrom('src/walks.rs', 'via_block_std')).toEqual([]);
});

it('reach the item of the module `super` and `crate` name, with no module between', () => {
expect(callsFrom('src/b/c.rs', 'via_bare_super')).toEqual(['function helper src/b/mod.rs:2']);
expect(callsFrom('src/b/c.rs', 'via_bare_crate')).toEqual(['function root_fn src/lib.rs:23']);
});

it('read `super` in an inline module as the module of its file', () => {
expect(callsFrom('src/x/tested.rs', 'via_inline_super').sort()).toEqual([
'function helper src/x/tested/shared.rs:1',
'function tested_helper src/x/tested.rs:3',
]);
const ids = cg.getNodesInFile('src/x/tested.rs').map((n) => n.id);
const imported = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports')
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
expect(imported).toEqual(['function tested_helper src/x/tested.rs:3']);
});

it('reach an item the module passes on with a `pub use`, or a glob of one', () => {
expect(callsFrom('src/walks.rs', 'via_reexport')).toEqual(['function make_list src/types/list.rs:1']);
expect(callsFrom('src/walks.rs', 'via_glob')).toEqual(['function list src/type_/prelude.rs:1']);
expect(callsFrom('src/walks.rs', 'via_macro_reexport')).toEqual(['function open src/plat/unix.rs:1']);
});

it('bind a `use crate::{Item}` to the crate root\'s item', () => {
const ids = cg.getNodesInFile('src/uses_config.rs').map((n) => n.id);
const imported = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'imports')
.map((e) => cg.getNode(e.target)!).map((t) => `${t.kind} ${t.qualifiedName} ${t.filePath}:${t.startLine}`);
expect(imported).toEqual(['struct Config src/lib.rs:21']);
});
});
Loading