Files
codegraph/__tests__/rust-path-same-file.test.ts
T
Colby MchenryandClaude Opus 5.5 505c355be3 fix(rust): a name written through a path is what the path names, not a same-file namesake (#2711)
A Rust name written through a path is an item of what the path names: a
module (`ast::Pattern`, `io::Error`), a type (`Source::Record { … }`) or a
crate (`mio::Interest`, `std::ffi::OsStr`). isRustNameInScope accepted any
candidate in the file the path is written in, whatever the path said, and
a std path only when the candidate was in another file:
- clap's `std::ffi::OsStr` linked to its own `struct OsStr`, and
  `io::Error`, `fmt::Error` and `fmt::Result` in `error/mod.rs` to its own
  `Error` and `Result`;
- tokio's `mio::Interest` linked to its own `Interest`, and
  `std::time::Instant` to its own `Instant`;
- serde's tests' `Token::Struct { … }` literals linked to their own
  `Struct`s, and serde_derive's `syn::Path { … }` to a `Default::Path`
  variant;
- gleam's `ast::Pattern` linked to the `enum Pattern` beside it, and
  `BinaryBuilderPosition::RecordAttribute { … }` to another enum's
  variant of that name.

Fix, in the path branch of isRustNameInScope:
- A path from std, core, alloc or a crate the manifests depend on names
  nothing of the project's, in any file, unless the project is that crate
  or a `use` in the file binds the root.
- The same-file shortcut is gone. A candidate is the path's when its
  file is the path's module, its qualified name holds the segment (a
  type's variant or method), or it sits in an inline `mod` of that name
  in the file (isInRustInlineModule; rustfmt closes the block at the
  indentation it opens at).
- Through a module a `use` binds, a candidate must be in a crate that
  `use`'s path can reach (rustUseCanReach, as for a bare name): tokio's
  `io::Handle` under `use crate::runtime::io` is tokio's own, not
  tokio-test's `io.rs` `Handle` (a dev-dependency).

Tests: rust-path-same-file.test.ts, 6 tests, on both engines; 5 fail
on main (the sixth is the inline-module guard), and the crate-reach one
also fails with only that rule switched off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:38:32 +00:00

184 lines
7.3 KiB
TypeScript

/**
* 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',
]);
});
});