Files
codegraph/__tests__/rust-path-same-file.test.ts
T
Colby MchenryandClaude Opus 5.5 e2aa5d205d fix(rust): a same-file path's inline module is read from its braces, not its indentation (#2713)
PR #2711 and PR #2702 landed a way each to tell which inline `mod` a
place in a Rust file is in. #2711's isInRustInlineModule scanned lines
upward for `mod <name> {` and trusted rustfmt's indentation to find where
the block closes. #2702's rustModBlocks scans the braces, skipping
strings, raw strings, char literals and comments, and keeps the result in
rustUsesOf. isInRustInlineModule now asks rustBlockAt for the innermost
block around the candidate, as rustDeclaredItems does, so there is one
answer to that question.

What changes:
- An item written without indentation inside `mod flat { … }` is found
  through `flat::Flat`. The indentation rule rejected it.
- `outer::Shell` is only `outer`'s own `Shell`, not one declared in a
  `mod deep` inside it. The upward scan accepted any item below the
  `mod outer {` line.
- A file whose braces don't all close says nothing, as it does for the
  `use` walk of PR #2702.

The A/B (main 818e7803 vs this change; axum, tokio, clap, serde,
ripgrep, gleam, dioxus, and the gin, serilog, flask and express
controls) is byte-identical. rust-path-same-file.test.ts gains the two
cases. The `flat::Flat` one fails on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:31:06 +00:00

210 lines
8.2 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',
'}',
'',
// Braces, not indentation, say what a module holds.
'mod flat {',
'pub struct Flat;',
'}',
'',
'pub fn flat() -> flat::Flat {',
' flat::Flat',
'}',
'',
// `outer::Shell` is `outer`'s own, not the one in `outer::deep`.
'mod outer {',
' pub struct Shell;',
' pub mod deep {',
' pub struct Shell;',
' }',
'}',
'',
'pub fn shell() -> outer::Shell {',
' todo!()',
'}',
'',
].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']);
});
it('reads an inline module from its braces: an item at any indentation, and not one in a module inside it', () => {
expect(targetsOn(cg, 'src/position.rs', 25, 'Flat')).toEqual(['struct Flat src/position.rs@22']);
expect(targetsOn(cg, 'src/position.rs', 36, 'Shell')).toEqual(['struct Shell src/position.rs@30']);
});
});
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',
]);
});
});