mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 12:00:02 +08:00
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5e72a43b3c
commit
505c355be3
@@ -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;
|
||||
// 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;
|
||||
// `io::Error` under `use std::io;` is std's, whatever `io/` directory the project has.
|
||||
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' || projectCrates.has(seg)) return true;
|
||||
// `io::Error` under `use std::io;` is std's, whatever `io/` directory the project has.
|
||||
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
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user