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 @@ -231,6 +231,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Rust, a type alias, struct, enum or function declared without `pub` now links only from its own module and the modules inside it, and a name a `use` brings in from one of your workspace's crates, like `use tokio_test::task::spawn` or `use crate::error::Error`, links only to an item of that crate or of a crate it depends on. Before, a file's `use crate::Result` could link its `Result<…>` types to another module's private `type Result`, as in dioxus's CLI, and a call to an imported function could link to another crate's function of the same name, such as tokio's test `spawn(…)` calls to one in its examples, or to nothing because a private function of that name was in the way. Re-index Rust projects after upgrading.
- In Rust, a name a `use` brings in, like `use crate::sync::watch::Receiver` or `use tokio::task::yield_now`, now links to the item of that name in the module its path leads to, found the way Rust finds it: declared there, in a module declared inline with `mod name { … }`, or passed on by that module's own `pub use`, including one written inside a macro like tokio's `cfg_sync! { … }`. Before, any item of that name in the crate could take it, so tokio's `Receiver<()>` linked to the `Receiver` of `net/unix/pipe.rs`, `crate::runtime::Handle` to the runtime driver's `Handle`, and `crate::runtime::task::JoinHandle` to a test mock. A name whose item is declared by a macro, which the index doesn't see, now links to nothing rather than to a same-named item elsewhere. Re-index Rust projects after upgrading.
- In Rust, a name written on its own, like an `Err(…)` call or a `Response` return type, or through a module, like `io::Error` or `use crate::de::Error`, no longer links to a `type` or `const` declared inside an `impl`, such as a `FromStr` impl's `type Err` or a `Service` impl's `type Response`: only a path like `Self::Err` names those, and they are now listed under their type, as `ValueHint::Err`. Before, serde's and ripgrep's `Ok(…)` and `Err(…)` calls linked to a serializer's or parser's `type Ok` or `type Err`, axum's handlers' `Response` to a `Service` impl's, and ripgrep's `impl Captures for RegexCaptures` to another impl's `type Captures` instead of the `Captures` trait. Re-index Rust projects after upgrading.
- In Rust, a name written through a path, like `io::Error`, `std::ffi::OsStr`, `mio::Interest`, `ast::Pattern` or `Source::Record { … }`, now links to what that path names, or to nothing when the path leads into the standard library or another crate, and no longer to an item of the same name that merely sits in the same file. Before, clap's `std::ffi::OsStr` and `io::Error` linked to its own `OsStr` and `Error`, serde's tests' `Token::Struct { … }` to their own `Struct`, and gleam's `ast::Pattern` to the `Pattern` enum beside it. Re-index Rust projects after upgrading.
- In C++, every name a declaration lists now has the declaration's type, like `strings_found` in google/breakpad's `ByteBuffer symbols_found, strings_found;` or `data_` in a class's `Table *index_, *data_;`, so calls on it link to that class's methods. Before, only the first name had its type read, and a call on another name linked to nothing or to the class of an earlier variable with the same name.
- In C++, a call on a parameter, a local lambda or functor, a lambda's parameter or a range-`for` variable no longer links to a function or method elsewhere that only shares its name, like protocolbuffers/protobuf's code generators calling `format(…)` on their `Formatter& format` parameter, which linked to the conformance suite's `TestResult::format`, or facebook/rocksdb's tests calling a local `auto key = [](int i) {…};`. C++ finds the local first, so that is what the call calls.

Expand Down
183 changes: 183 additions & 0 deletions __tests__/rust-path-same-file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* A Rust name written through a path is an item of what the path names: a
* module (`ast::Pattern`, `io::Error`), a type (`Position::Record { … }`) or a
* crate (`mio::Interest`, `std::ffi::OsStr`). An item of the file the path is
* written in is that only when the path leads there: an inline `mod` of the
* file, the item's own type, or the file's own module. A path's file was
* enough before: clap's `std::ffi::OsStr` linked to its own `struct OsStr`,
* its `io::Error` and `fmt::Error` to `error/mod.rs`'s `struct Error`, tokio's
* `mio::Interest` to its own `Interest`, gleam's `ast::Pattern` to the
* `enum Pattern` beside it, and a `Source::Record { … }` to another enum's
* `Record` variant.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

/** What `file` links `name` to on `line`, as `kind qualifiedName file@line`, sorted. */
const targetsOn = (cg: CodeGraph, file: string, line: number, name: string): string[] => {
const out: string[] = [];
for (const node of cg.getNodesInFile(file)) {
for (const edge of cg.getOutgoingEdges(node.id)) {
if (edge.line !== line) continue;
const target = cg.getNode(edge.target)!;
if (target.name === name) out.push(`${target.kind} ${target.qualifiedName} ${target.filePath}@${target.startLine}`);
}
}
return [...new Set(out)].sort();
};

describe('a Rust path into another module, type or crate', () => {
let root = '';
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-path-file-'));
const files: Record<string, string> = {
'Cargo.toml': '[package]\nname = "rt"\nversion = "0.1.0"\n\n[dependencies]\nmio = "1"\n',
'src/lib.rs': 'pub mod ast;\npub mod error;\npub mod exhaustiveness;\npub mod interest;\npub mod os_str;\npub mod position;\n',
'src/ast.rs': 'pub enum Pattern {\n Int,\n Discard,\n}\n',
// gleam: `ast::Pattern` is the ast module's, not this file's own `Pattern`.
'src/exhaustiveness.rs': [
'use crate::ast;',
'',
'pub enum Pattern {',
' Variable,',
'}',
'',
'pub fn check(pattern: &ast::Pattern) -> bool {',
' true',
'}',
'',
'pub fn own() -> Pattern {',
' Pattern::Variable',
'}',
'',
].join('\n'),
// clap: `io::Error`, `fmt::Error` and `fmt::Result` are std's, though this file is `error`.
'src/error/mod.rs': [
'use std::{error, fmt, io};',
'',
'pub type Result<T, E = Error> = std::result::Result<T, E>;',
'',
'pub struct Error;',
'',
'impl From<io::Error> for Error {',
' fn from(e: io::Error) -> Self {',
' Error',
' }',
'}',
'',
'impl fmt::Display for Error {',
' fn fmt(&self, f: &mut fmt::Formatter<\'_>) -> fmt::Result {',
' Ok(())',
' }',
'}',
'',
'impl error::Error for Error {}',
'',
].join('\n'),
// tokio: `mio::Interest` is the mio crate's.
'src/interest.rs': 'pub struct Interest(usize);\n\nimpl Interest {\n pub fn to_mio(self) -> mio::Interest {\n todo!()\n }\n}\n',
// clap: `std::ffi::OsStr` is std's.
'src/os_str.rs': 'pub struct OsStr;\n\nimpl OsStr {\n pub fn as_os_str(&self) -> &std::ffi::OsStr {\n todo!()\n }\n}\n',
// gleam: two enums share a variant name; a path names its own enum's.
'src/position.rs': [
'pub enum Source {',
' Record { first: bool },',
'}',
'',
'pub enum Builder {',
' Record { fields: usize },',
'}',
'',
'pub fn source() -> Source {',
' Source::Record { first: true }',
'}',
'',
'mod inner {',
' pub struct Cell;',
'}',
'',
'pub fn cell() -> inner::Cell {',
' inner::Cell',
'}',
'',
].join('\n'),
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
}, 60_000);

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

it('names the module a path writes, not an item of the file of that name', () => {
expect(targetsOn(cg, 'src/exhaustiveness.rs', 7, 'Pattern')).toEqual(['enum Pattern src/ast.rs@1']);
expect(targetsOn(cg, 'src/exhaustiveness.rs', 11, 'Pattern')).toEqual(['enum Pattern src/exhaustiveness.rs@3']);
});

it('names std through `io::`, `fmt::` and `error::`, whatever the file is called', () => {
expect(targetsOn(cg, 'src/error/mod.rs', 8, 'Error')).toEqual([]);
expect(targetsOn(cg, 'src/error/mod.rs', 14, 'Result')).toEqual([]);
expect(targetsOn(cg, 'src/error/mod.rs', 19, 'Error')).toEqual([]);
});

it('names an outside crate through its own name or `std::`', () => {
expect(targetsOn(cg, 'src/interest.rs', 4, 'Interest')).toEqual([]);
expect(targetsOn(cg, 'src/os_str.rs', 4, 'OsStr')).toEqual([]);
});

it('names the variant of the enum the path writes', () => {
expect(targetsOn(cg, 'src/position.rs', 10, 'Record')).toEqual(['enum_member Source::Record src/position.rs@2']);
});

it('still names an inline module\'s item', () => {
expect(targetsOn(cg, 'src/position.rs', 17, 'Cell')).toEqual(['struct Cell src/position.rs@14']);
});
});

describe('a Rust path through a module a `use` binds', () => {
let root = '';
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-path-use-'));
const files: Record<string, string> = {
'Cargo.toml': '[workspace]\nmembers = ["rt", "rt-test"]\n',
'rt/Cargo.toml': '[package]\nname = "rt"\nversion = "0.1.0"\n\n[dev-dependencies]\nrt-test = { path = "../rt-test" }\n',
'rt/src/lib.rs': 'pub mod runtime;\n',
'rt/src/runtime/mod.rs': 'pub mod io;\npub mod signal;\n',
'rt/src/runtime/io/mod.rs': 'pub struct Handle;\n',
// tokio: `io::Handle` under `use crate::runtime::io` is tokio's own, not tokio-test's.
'rt/src/runtime/signal.rs': 'use crate::runtime::io;\n\npub struct Handle;\n\npub fn new(io_handle: &io::Handle) -> Handle {\n Handle\n}\n',
'rt-test/Cargo.toml': '[package]\nname = "rt-test"\nversion = "0.1.0"\n\n[dependencies]\nrt = { path = "../rt" }\n',
'rt-test/src/lib.rs': 'pub mod io;\n',
'rt-test/src/io.rs': 'pub struct Handle;\n',
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
}, 60_000);

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

it('names an item of a crate that `use`\'s path can reach', () => {
expect(targetsOn(cg, 'rt/src/runtime/signal.rs', 5, 'Handle')).toEqual([
'struct Handle rt/src/runtime/io/mod.rs@1',
'struct Handle rt/src/runtime/signal.rs@3',
]);
});
});
47 changes: 42 additions & 5 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8656,15 +8656,29 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context:
// `<T as FromStr>::Err`. No module path names one: axum-core's
// `std::error::Error` is std's, not its file's `impl IntoResponseParts`'s `type Error`.
if (isRustAssociatedItem(candidate)) return seg === '' || (seg === 'Self' && isInEnclosingRustImpl(candidate, ref, context));
if ((root === 'std' || root === 'core' || root === 'alloc') && candidate.filePath !== ref.filePath) return false;
const projectCrates = rustProjectCrates(context);
const pathUses = rustUsesOf(ref.filePath, context);
// A path from std or a crate the manifests depend on is that crate's, in
// the file of a namesake too: clap's `std::ffi::OsStr` is not its own
// `struct OsStr`, tokio's `mio::Interest` not its own `Interest`.
if ((root === 'std' || root === 'core' || root === 'alloc' || rustDependencyCrates(context).has(root)) &&
!projectCrates.has(root) && !pathUses.bound.has(root)) return false;
// A project crate's name re-exports as `crate::` does: `clap::Command` is clap_builder's.
if (seg === '' || seg === 'crate' || seg === 'self' || seg === 'super' || seg === 'Self' || candidate.filePath === ref.filePath ||
rustProjectCrates(context).has(seg)) return true;
if (seg === '' || seg === 'crate' || seg === 'self' || seg === 'super' || seg === 'Self' || projectCrates.has(seg)) return true;
// `io::Error` under `use std::io;` is std's, whatever `io/` directory the project has.
const pathUses = rustUsesOf(ref.filePath, context);
if (pathUses.external.has(seg) && !pathUses.bound.has(seg)) return false;
// Through a module a `use` binds, an item of a crate that `use`'s path can
// reach: tokio's `io::Handle` under `use crate::runtime::io` is tokio's,
// not tokio-test's `io.rs` `Handle`.
const from = pathUses.boundFrom.get(root);
if (from && !rustUseCanReach(from, candidate, ref, context)) return false;
// Otherwise the module or type the path names, wherever it is written:
// gleam's `ast::Pattern` is `ast.rs`'s, not the `enum Pattern` beside it,
// and `Source::Record { … }` `Source`'s variant, not another enum's. The
// file's own items are reached so only from an inline `mod` of the file.
return rustModuleName(candidate.filePath) === seg || candidate.filePath.includes(`/${seg}/`) ||
candidate.qualifiedName.split('::').includes(seg);
candidate.qualifiedName.split('::').includes(seg) ||
(candidate.filePath === ref.filePath && isInRustInlineModule(candidate, seg, context));
}
// Nor does its name alone: clap's `Err(…)` calls went to the `type Err` of
// a `FromStr` impl in the same file, axum's routing tests' `Response`s to
Expand Down Expand Up @@ -9091,6 +9105,29 @@ function matchRustSelfPath(name: string, ref: UnresolvedRef, context: Resolution
return { original: ref, targetNodeId: target.id, confidence: target.filePath === ref.filePath ? 0.9 : 0.8, resolvedBy: 'qualified-name' };
}

/**
* Whether `candidate` sits in an inline `mod <name> { … }` of its own file —
* the only way a path through `name` reaches it, since an inline module adds
* nothing to its items' qualified names. rustfmt closes the block at the
* indentation it opens at.
*/
function isInRustInlineModule(candidate: Node, name: string, context: ResolutionContext): boolean {
const lines = context.getFileLines?.(candidate.filePath) ?? context.readFile(candidate.filePath)?.split(/\r?\n/) ?? [];
const head = new RegExp(`^(\\s*)(?:pub(?:\\([^)]*\\))?\\s+)?mod\\s+${name}\\s*\\{`);
for (let i = candidate.startLine - 2; i >= 0; i--) {
const open = head.exec(lines[i] ?? '');
if (!open) continue;
const indent = open[1]!.length;
if (candidate.startColumn <= indent) return false;
for (let j = i + 1; j < candidate.startLine - 1; j++) {
const closer = /^(\s*)\}/.exec(lines[j] ?? '');
if (closer && closer[1]!.length <= indent) return false;
}
return true;
}
return false;
}

/** Whether `candidate` is declared in the same `impl` / `trait` block as `ref`, above it. */
function isInEnclosingRustImpl(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
const header = rustEnclosingImplLine(ref, context);
Expand Down