mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 11:24:49 +08:00
fix(rust): a trait's associated type named <T as Trait>::X or bound as Trait<X = …> links only to what it names (#2712)
A trait's associated type is named in two more ways than `Self::X`, and
neither names a module item:
- Through a qualified self, `<T as Trait>::X`. isRustNameInScope took any
associated item of the name for it. axum's `<L as Listener>::Io`, with
`L` a generic, went to `ReadyListener`'s `type Io`. A trait's own
`fn call(…) -> <Self as HandlerCallWithExtractors<T, S>>::Future;`
went to an impl's `type Future` further down the file.
- As a binding in the trait's arguments, `Iterator<Item = &str>` or
`Readable<Storage = UnsyncStorage>`. The name was matched like a bare
type. axum's `impl Iterator<Item = &str>` went to a same-file
`enum Item`, and dioxus's `Storage = …` bindings to the `Storage` trait.
Fix, in isRustNameInScope:
- isRustAssocBinding: a name followed by `=` or `:` inside an unclosed
`<` on its line is a binding or bound, and links nothing. The trait's
own associated type is no node.
- rustQualifiedSelf reads `<T as Trait>::X` (and `<T>::X`) as its self
type and trait, each as the last segment of its path:
- `Self` means `Self::X`: the enclosing impl's own item, nothing in a
trait's body;
- a generic means no impl's;
- a type means its impl of that trait (the candidate's
`Owner::Name` must match, and its impl header's trait too).
Tests: rust-assoc-qualified-self.test.ts, 3 tests, on both engines; all
3 fail before the fix, and switching off either rule fails its tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6fe9dc1266
commit
818e780336
@@ -233,6 +233,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- 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 Rust, a trait's associated type written as `<T as Trait>::Name`, or bound in a trait's arguments like `Iterator<Item = &str>`, now links only to what it can name: through `<Self as Trait>` the `impl`'s own, through a type that type's `impl` of the trait, and nothing through a generic parameter or from a binding. Before, axum's `<L as Listener>::Io` linked to one listener's `type Io`, its `Iterator<Item = &str>` to an unrelated `enum Item`, and dioxus's `Storage = …` bindings to a `Storage` trait. 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.
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* A trait's associated type is named in two more ways than `Self::X`:
|
||||
* through a qualified self, `<T as Trait>::X`, and as a binding in a trait's
|
||||
* arguments, `Iterator<Item = T>` or `Readable<Storage = S>`. Neither names
|
||||
* a module item, and the first names an impl's own only when its self is
|
||||
* `Self` in that impl or the impl's type. axum's `<L as Listener>::Io`, with
|
||||
* `L` a generic, went to `ReadyListener`'s `type Io`; a trait's own `<Self
|
||||
* as HandlerCallWithExtractors<T, S>>::Future` to an impl's `type Future`;
|
||||
* axum's `Iterator<Item = &str>` to a same-file `enum Item`, and dioxus's
|
||||
* `Storage = UnsyncStorage` bindings to the `Storage` trait.
|
||||
*/
|
||||
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 trait\'s associated type, named through its trait', () => {
|
||||
let root = '';
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-assoc-qself-'));
|
||||
const files: Record<string, string> = {
|
||||
'Cargo.toml': '[package]\nname = "web"\nversion = "0.1.0"\n',
|
||||
'src/lib.rs': 'pub mod handler;\npub mod serve;\npub mod strip;\n',
|
||||
// A trait's own `<Self as …>::Future` is the trait's, not an impl's below it.
|
||||
'src/handler.rs': [
|
||||
'pub trait Handler {',
|
||||
' type Future;',
|
||||
'',
|
||||
' fn call(self) -> <Self as Handler>::Future;',
|
||||
'}',
|
||||
'',
|
||||
'pub struct IntoHandler;',
|
||||
'',
|
||||
'impl Handler for IntoHandler {',
|
||||
' type Future = u8;',
|
||||
'',
|
||||
' fn call(self) -> <Self as Handler>::Future {',
|
||||
' 0',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
// `<L as Listener>::Io` with `L` a generic names no impl's.
|
||||
'src/serve.rs': [
|
||||
'pub trait Listener {',
|
||||
' type Io;',
|
||||
'}',
|
||||
'',
|
||||
'pub struct Ready;',
|
||||
'',
|
||||
'impl Listener for Ready {',
|
||||
' type Io = u16;',
|
||||
'}',
|
||||
'',
|
||||
'pub fn handle<L: Listener>(io: <L as Listener>::Io) {}',
|
||||
'',
|
||||
'pub fn ready(io: <Ready as Listener>::Io) {}',
|
||||
'',
|
||||
].join('\n'),
|
||||
// A binding names the trait's associated type, not a same-file `enum Item`.
|
||||
'src/strip.rs': [
|
||||
'pub enum Item<T> {',
|
||||
' Both(T, T),',
|
||||
'}',
|
||||
'',
|
||||
'pub fn segments(s: &str) -> impl Iterator<Item = &str> {',
|
||||
' s.split(\'/\')',
|
||||
'}',
|
||||
'',
|
||||
'pub fn zip(a: Vec<u8>) -> impl Iterator<Item = Item<u8>> {',
|
||||
' a.into_iter().map(|x| Item::Both(x, x))',
|
||||
'}',
|
||||
'',
|
||||
].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('reads `<Self as Trait>::X` as `Self::X`: the impl\'s own in its impl, nothing in the trait', () => {
|
||||
expect(targetsOn(cg, 'src/handler.rs', 4, 'Future')).toEqual([]);
|
||||
expect(targetsOn(cg, 'src/handler.rs', 12, 'Future')).toEqual(['type_alias IntoHandler::Future src/handler.rs@10']);
|
||||
});
|
||||
|
||||
it('links nothing through a generic, and the type\'s own impl through a concrete type', () => {
|
||||
expect(targetsOn(cg, 'src/serve.rs', 11, 'Io')).toEqual([]);
|
||||
expect(targetsOn(cg, 'src/serve.rs', 13, 'Io')).toEqual(['type_alias Ready::Io src/serve.rs@8']);
|
||||
});
|
||||
|
||||
it('links nothing from a binding, while the type it binds to still links', () => {
|
||||
expect(targetsOn(cg, 'src/strip.rs', 5, 'Item')).toEqual([]);
|
||||
expect(targetsOn(cg, 'src/strip.rs', 9, 'Item')).toEqual(['enum Item src/strip.rs@1']);
|
||||
});
|
||||
});
|
||||
@@ -8629,6 +8629,11 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context:
|
||||
// Bare in the SOURCE: the index keeps `crate::error::Result` by its last
|
||||
// segment, and a path is not a prelude lookup.
|
||||
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
|
||||
// A trait's associated type bound in its arguments — `Iterator<Item = &str>`,
|
||||
// `Readable<Storage = S>` — is that trait's, which no node stands for: never
|
||||
// a module item or an impl's. axum's `Iterator<Item = &str>` went to a
|
||||
// same-file `enum Item`, dioxus's `Storage = UnsyncStorage` to a `Storage` trait.
|
||||
if (ref.referenceKind === 'references' && line !== undefined && isRustAssocBinding(line, ref.column, name)) return false;
|
||||
// A variant path — `Mode::A`, `mode::Mode::B`, a `Mode::C(x)` pattern, or
|
||||
// `Self::A` in an impl, where the reference names the impl's type (#2328) —
|
||||
// reads the enum that declares that variant, and nothing else: ripgrep's
|
||||
@@ -8664,7 +8669,19 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context:
|
||||
// An associated item, only through its type: `Self::Err` in its impl,
|
||||
// `<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 (isRustAssociatedItem(candidate)) {
|
||||
if (seg === 'Self') return isInEnclosingRustImpl(candidate, ref, context);
|
||||
if (seg !== '' || line === undefined) return false;
|
||||
// A qualified self reads as its type: `<Self as Handler>::Future` is
|
||||
// `Self::Future` — in a trait's own body, the trait's, no impl's — a
|
||||
// generic's (`<L as Listener>::Io`) no impl's, and `<Ready as
|
||||
// Listener>::Io` that type's impl of that trait.
|
||||
const qself = rustQualifiedSelf(line.slice(0, ref.column));
|
||||
if (!qself) return true;
|
||||
if (qself.self === 'Self') return isInEnclosingRustImpl(candidate, ref, context);
|
||||
if (isRustGenericParam(qself.self, ref, context) || candidate.qualifiedName !== `${qself.self}::${candidate.name}`) return false;
|
||||
return qself.trait === null || rustTraitImplHeader(candidate, context)?.trait === qself.trait;
|
||||
}
|
||||
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
|
||||
@@ -9144,6 +9161,54 @@ function isInEnclosingRustImpl(candidate: Node, ref: UnresolvedRef, context: Res
|
||||
}
|
||||
|
||||
/** Whether `name` is a generic type parameter of the function or impl around `ref` (`fn f<V: Visitor>`, `impl<'de, E>`). */
|
||||
/**
|
||||
* Whether `name`, at `column` of `line`, is a trait's associated type bound
|
||||
* in that trait's arguments: `Iterator<Item = &str>`, `Writable<M: 'static>`.
|
||||
* An unclosed `<` before it on the line says it is inside them.
|
||||
*/
|
||||
function isRustAssocBinding(line: string, column: number, name: string): boolean {
|
||||
if (!line.startsWith(name, column) || !/^\s*(?:=(?![=>])|:(?!:))/.test(line.slice(column + name.length))) return false;
|
||||
let angles = 0;
|
||||
let parens = 0;
|
||||
for (let i = column - 1; i >= 0; i--) {
|
||||
const c = line[i];
|
||||
if (c === '>' && line[i - 1] !== '-') angles++;
|
||||
else if (c === '<') {
|
||||
if (angles === 0) return parens === 0;
|
||||
angles--;
|
||||
} else if (c === ')') parens++;
|
||||
else if (c === '(') {
|
||||
if (parens === 0) return false;
|
||||
parens--;
|
||||
} else if (c === '{' || c === '}' || c === ';') return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The qualified self a path ends in, written right before `::X`: `<T as
|
||||
* Trait<A>>::X` gives `T` and `Trait`, `<T>::X` gives `T` and no trait —
|
||||
* each as the last segment of its path, without generics. Null when the text
|
||||
* is no qualified self (a turbofish, `Vec::<T>::new`, is not one).
|
||||
*/
|
||||
function rustQualifiedSelf(before: string): { self: string; trait: string | null } | null {
|
||||
const text = before.replace(/\s*::\s*$/, '');
|
||||
if (!text.endsWith('>')) return null;
|
||||
let depth = 0;
|
||||
let open = -1;
|
||||
for (let i = text.length - 1; i >= 0 && open < 0; i--) {
|
||||
if (text[i] === '>' && text[i - 1] !== '-') depth++;
|
||||
else if (text[i] === '<' && --depth === 0) open = i;
|
||||
}
|
||||
if (open < 0 || /::\s*$/.test(text.slice(0, open))) return null;
|
||||
const inner = text.slice(open + 1, -1).trim();
|
||||
const head = (part: string): string =>
|
||||
/((?:[A-Za-z_]\w*\s*::\s*)*)([A-Za-z_]\w*)\s*(?:<[\s\S]*)?$/.exec(part.replace(/^(?:&\s*(?:'\w+\s+)?(?:mut\s+)?|dyn\s+)+/, '').trim())?.[2] ?? '';
|
||||
const as = /\s+as\s+/.exec(inner);
|
||||
const self = head(as ? inner.slice(0, as.index) : inner);
|
||||
return self ? { self, trait: as ? head(inner.slice(as.index + as[0].length)) || null : null } : null;
|
||||
}
|
||||
|
||||
function isRustGenericParam(name: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
if (!/^[A-Z]\w*$/.test(name)) return false;
|
||||
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
|
||||
|
||||
Reference in New Issue
Block a user