mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 19:30:10 +08:00
fix(rust): an impl's associated type or const no longer takes a bare name or a module path (#2707)
A `type` or `const` written inside an `impl` belongs to the type: Rust
reaches it only through a path like `Self::Err`, `<T as FromStr>::Err` or
`T::Err`, never by its name alone, and no module path or `use` names it.
Both extractors put it at file scope under its bare name, so the resolver
could not tell it from a module's own alias:
- a same-file `Err(…)` call linked to a `FromStr` impl's `type Err`, and
serde's `Ok(…)` calls to a serializer's `type Ok`;
- axum's routing tests' `Response` linked to the `type Response` of
`routing/mod.rs`'s `impl Service`, a private item the modules inside
it can see;
- `io::Error` and `std::error::Error` linked to the `type Error` of an
impl in the same file, and serde's `use crate::de::Error` to one in
`de/mod.rs`.
Fix:
- Both extractors (rustImplItemOwner, and the kernel's impl_item_owner)
qualify an item written directly in an impl by the impl's type, as they
already qualify its methods: `ValueHint::Err`, `Limits::MAX`. A type no
single name stands for keeps the impl's spelling, whitespace collapsed:
`(A, B)::First`, `[u8]::Out`, `u64::WIDTH`.
- isRustAssociatedItem: a Rust type alias or const with a qualified name.
Nothing else of those kinds gets one: an inline module adds no prefix,
a function body's items aren't indexed, and a trait's default `type`
is already its trait's.
- isRustNameInScope: such an item never matches a bare name, and matches a
path only as `Self::X` in its own impl or as `<…>::X` (unchanged).
- resolveRustPathReference never picks one for a `crate::m::X` path.
Tests: rust-associated-item-scope.test.ts, 6 tests, on both engines; 5
fail on origin/main. The kernel parity torture fixture gains associated
items on a tuple, a slice reference and a primitive.
A/B on fresh indexes, origin/main 5530676f vs this change. Every changed
edge was triaged; no right link was lost:
- axum: 8 wrong links gone, 14 moved to axum-core's `pub type Response`.
- tokio: 46 wrong links gone.
- clap: 12 wrong links gone, 7 of them `Err(…)` calls.
- serde: 98 wrong links gone; `First<…>`/`Second<…>` now reach the
module's aliases.
- ripgrep: 54 wrong links gone; `impl Captures for RegexCaptures` (3)
now implements grep-matcher's `Captures` trait.
- gleam: 5 wrong links gone, 10 moved to the right target, 1 moved from
one wrong target to another.
- dioxus: 71 wrong links gone, 40 moved to the right target, 3 moved from
one wrong target to another.
- Controls (gin, serilog, flask, express) byte-identical; indexing time
unchanged within noise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6c49bfa667
commit
b5553e238a
@@ -217,6 +217,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Rust, a call like `String::from("…")`, `Vec::from([…])` or `NonNull::from(x)` no longer links to your project's own `impl From<YourType> for String` (or for `Vec`, `NonNull`, …) unless what it passes is a `YourType`. A method call on a standard-library value, like `files.iter()` or `buf.extend(…)`, links to your trait's implementation for that type only where the trait is imported. Before, every `String::from("…")` call in dioxus's CLI linked to one such conversion, and tokio's `vec.iter()` calls to an internal trait's `Vec` impl. Re-index Rust projects after upgrading.
|
||||
- In Rust, a method called through `self: Pin<&mut Self>`, `self: Box<Self>`, `self: Rc<Self>` or `self: Arc<Self>`, or on a value declared `Pin<&mut …>`, now links the way Rust looks it up: to the type's own method, like `self.poll_flush(cx)` inside tokio's `impl AsyncWrite for File`, rather than to a catch-all `impl AsyncWrite for Pin<P>`, while `Pin`'s own methods, like `self.get_mut()`, stay unlinked. A type's own method now also comes before a trait's of the same name, so tokio's `Arc::from_raw(…)` calls are the standard library's rather than its internal `Link` trait's, and a method called on a tuple, like serde's `(self.ip(), self.port()).serialize(…)`, no longer links to an unrelated type's. Before, most calls made through such a `self` linked to nothing. Re-index Rust projects after upgrading.
|
||||
- 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 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 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.
|
||||
|
||||
## [1.6.2] - 2026-10-03
|
||||
|
||||
@@ -182,6 +182,22 @@ impl Render for dyn Base {
|
||||
|
||||
impl Base for u32 {}
|
||||
|
||||
// Associated items are qualified by the impl's type; one no single name
|
||||
// stands for keeps the impl's spelling, whitespace collapsed.
|
||||
impl Render for (u32,
|
||||
u32) {
|
||||
type Out = u8;
|
||||
const PAIR: usize = 2;
|
||||
}
|
||||
|
||||
impl<'a> Render for &'a [u8] {
|
||||
type Out = u16;
|
||||
}
|
||||
|
||||
impl Render for u64 {
|
||||
const WIDTH: usize = 8;
|
||||
}
|
||||
|
||||
impl Later {
|
||||
fn touch(&self) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* A Rust associated item — a `type` or `const` written inside an `impl` (or a
|
||||
* trait) — belongs to the type, not to the module: Rust reaches it only
|
||||
* through a path (`Self::Err`, `<T as FromStr>::Err`, `T::Err`), never by its
|
||||
* name alone, and no module path names it. clap's and gleam's `Err(…)` calls
|
||||
* went to the `type Err` of a `FromStr` impl in the same file, and axum's
|
||||
* `routing/` tests' `Response`s to the `type Response` of the `Service` impl
|
||||
* in `routing/mod.rs`, whose private items the modules inside it can see.
|
||||
*/
|
||||
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 `name`, written in `file`, links to, as `file@line` of each target, sorted. */
|
||||
const targetsOf = (cg: CodeGraph, file: string, name: string, kinds: string[]): string[] => {
|
||||
const out: string[] = [];
|
||||
for (const node of cg.getNodesInFile(file)) {
|
||||
for (const edge of cg.getOutgoingEdges(node.id)) {
|
||||
if (!kinds.includes(edge.kind)) continue;
|
||||
const target = cg.getNode(edge.target)!;
|
||||
if (target.name === name) out.push(`${target.filePath}@${target.startLine}`);
|
||||
}
|
||||
}
|
||||
return [...new Set(out)].sort();
|
||||
};
|
||||
|
||||
describe('a Rust associated item', () => {
|
||||
let root = '';
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rust-assoc-'));
|
||||
const files: Record<string, string> = {
|
||||
'Cargo.toml': '[package]\nname = "web"\nversion = "0.1.0"\n',
|
||||
'src/lib.rs': 'pub mod de;\npub mod error;\npub mod parts;\npub mod response;\npub mod routing;\npub mod value;\npub mod value_hint;\n',
|
||||
// serde declares `de::Error` in a macro, which the index doesn't read:
|
||||
// the one `Error` it holds for `de/mod.rs` is an impl's.
|
||||
'src/de/mod.rs': [
|
||||
'macro_rules! declare_error_trait {',
|
||||
' () => {',
|
||||
' pub trait Error: Sized {}',
|
||||
' };',
|
||||
'}',
|
||||
'',
|
||||
'declare_error_trait!();',
|
||||
'',
|
||||
'pub trait SeqAccess {',
|
||||
' type Error;',
|
||||
'}',
|
||||
'',
|
||||
'pub struct Access;',
|
||||
'',
|
||||
'impl SeqAccess for Access {',
|
||||
' type Error = String;',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'src/value.rs': 'use crate::de::Error;\n\npub struct ValueError;\n\nimpl crate::de::Error for ValueError {}\n',
|
||||
'src/response.rs': 'pub struct Body;\n\npub type Response<T = Body> = http::Response<T>;\n',
|
||||
// The crate's own `Error`, which `parts.rs`'s `std::error::Error` is not either.
|
||||
'src/error.rs': 'pub struct Error;\n',
|
||||
'src/routing/mod.rs': [
|
||||
'mod route;',
|
||||
'#[cfg(test)]',
|
||||
'mod tests;',
|
||||
'',
|
||||
'use crate::response::Response;',
|
||||
'use std::convert::Infallible;',
|
||||
'',
|
||||
'pub struct Router;',
|
||||
'',
|
||||
'impl<B> tower::Service<http::Request<B>> for Router {',
|
||||
' type Response = Response;',
|
||||
' type Error = Infallible;',
|
||||
'',
|
||||
' fn call(&mut self, req: http::Request<B>) -> Result<Self::Response, Self::Error> {',
|
||||
' todo!()',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
// A module inside `routing` sees its private items, but never an impl's.
|
||||
'src/routing/tests.rs': 'use crate::response::Response;\n\nfn handler() -> Response {\n todo!()\n}\n',
|
||||
'src/routing/route.rs': [
|
||||
'use crate::response::Response;',
|
||||
'',
|
||||
'pub struct Route;',
|
||||
'',
|
||||
'impl<B> tower::Service<http::Request<B>> for Route {',
|
||||
' type Response = Response;',
|
||||
'',
|
||||
' fn call(&mut self, req: http::Request<B>) -> Self::Response {',
|
||||
' todo!()',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'pub struct RouteFuture;',
|
||||
'',
|
||||
'impl RouteFuture {',
|
||||
' pub fn new(response: Response) -> Self {',
|
||||
' RouteFuture',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'src/value_hint.rs': [
|
||||
'use std::str::FromStr;',
|
||||
'',
|
||||
'pub enum ValueHint {',
|
||||
' Unknown,',
|
||||
' Other,',
|
||||
'}',
|
||||
'',
|
||||
'impl FromStr for ValueHint {',
|
||||
' type Err = String;',
|
||||
'',
|
||||
' fn from_str(s: &str) -> Result<Self, Self::Err> {',
|
||||
' match s {',
|
||||
' "other" => Ok(ValueHint::Other),',
|
||||
' _ => Err(format!("unknown hint: {s}")),',
|
||||
' }',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'pub fn parse(s: &str) -> Result<ValueHint, String> {',
|
||||
' if s.is_empty() {',
|
||||
' return Err(String::from("empty"));',
|
||||
' }',
|
||||
' s.parse()',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'src/parts.rs': [
|
||||
'use std::convert::Infallible;',
|
||||
'',
|
||||
'pub trait IntoParts {',
|
||||
' type Error;',
|
||||
'}',
|
||||
'',
|
||||
'pub struct Extensions;',
|
||||
'',
|
||||
'impl IntoParts for Extensions {',
|
||||
' type Error = Infallible;',
|
||||
'}',
|
||||
'',
|
||||
'#[derive(Debug)]',
|
||||
'pub struct HeaderError;',
|
||||
'',
|
||||
'impl std::error::Error for HeaderError {',
|
||||
' fn source(&self) -> Option<&(dyn std::error::Error + \'static)> {',
|
||||
' None',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'pub struct Jar;',
|
||||
'',
|
||||
'pub struct JarIter;',
|
||||
'',
|
||||
'impl Jar {',
|
||||
' pub fn iter(&self) -> impl Iterator<Item = String> + \'_ {',
|
||||
' JarIter',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'impl Iterator for JarIter {',
|
||||
' type Item = String;',
|
||||
'',
|
||||
' fn next(&mut self) -> Option<Self::Item> {',
|
||||
' None',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].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('is not what a bare call names: `Err(…)` is the prelude\'s, not a `FromStr` impl\'s `type Err`', () => {
|
||||
expect(targetsOf(cg, 'src/value_hint.rs', 'Err', ['calls'])).toEqual([]);
|
||||
});
|
||||
|
||||
it('is not what a bare type names, in its own file or a module inside its own', () => {
|
||||
expect(targetsOf(cg, 'src/routing/tests.rs', 'Response', ['references'])).toEqual(['src/response.rs@3']);
|
||||
const own = cg.getNodesInFile('src/routing/route.rs').find((n) => n.name === 'new')!;
|
||||
const targets = cg.getOutgoingEdges(own.id).filter((e) => e.kind === 'references').map((e) => cg.getNode(e.target)!)
|
||||
.filter((t) => t.name === 'Response').map((t) => `${t.filePath}@${t.startLine}`);
|
||||
expect(targets).toEqual(['src/response.rs@3']);
|
||||
});
|
||||
|
||||
it('is not what a module path names: `std::error::Error` is no impl\'s `type Error`', () => {
|
||||
const source = cg.getNodesInFile('src/parts.rs').find((n) => n.name === 'source')!;
|
||||
const targets = cg.getOutgoingEdges(source.id).map((e) => cg.getNode(e.target)!).filter((t) => t.name === 'Error');
|
||||
expect(targets.map((t) => `${t.kind} ${t.filePath}@${t.startLine}`)).toEqual([]);
|
||||
});
|
||||
|
||||
it('is not what a `use` or a path to its module names', () => {
|
||||
const value = cg.getNodesInFile('src/value.rs');
|
||||
const targets = value.flatMap((n) => cg.getOutgoingEdges(n.id)).map((e) => cg.getNode(e.target)!)
|
||||
.filter((t) => t.name === 'Error').map((t) => `${t.kind} ${t.filePath}@${t.startLine}`);
|
||||
expect(targets).toEqual([]);
|
||||
});
|
||||
|
||||
it('is not the `Item` an `Iterator<Item = …>` bound names', () => {
|
||||
const iter = cg.getNodesInFile('src/parts.rs').find((n) => n.name === 'iter')!;
|
||||
const targets = cg.getOutgoingEdges(iter.id).map((e) => cg.getNode(e.target)!).filter((t) => t.name === 'Item');
|
||||
expect(targets).toEqual([]);
|
||||
});
|
||||
|
||||
it('is still what `Self::` names inside its impl', () => {
|
||||
expect(targetsOf(cg, 'src/value_hint.rs', 'Err', ['references'])).toEqual(['src/value_hint.rs@9']);
|
||||
const call = cg.getNodesInFile('src/routing/mod.rs').find((n) => n.name === 'call')!;
|
||||
const targets = cg.getOutgoingEdges(call.id).filter((e) => e.kind === 'references').map((e) => cg.getNode(e.target)!)
|
||||
.map((t) => `${t.name} ${t.filePath}@${t.startLine}`).sort();
|
||||
expect(targets).toEqual(['Error src/routing/mod.rs@12', 'Response src/routing/mod.rs@11']);
|
||||
expect(targetsOf(cg, 'src/parts.rs', 'Item', ['references'])).toEqual(['src/parts.rs@33']);
|
||||
});
|
||||
});
|
||||
@@ -446,6 +446,26 @@ impl<'t> Walker<'t> {
|
||||
None
|
||||
}
|
||||
|
||||
/// rustImplItemOwner (languages/rust.ts) — the type an item written
|
||||
/// directly in an impl block belongs to: `ValueHint` for the `type Err`
|
||||
/// of `impl FromStr for ValueHint`, then `ValueHint::Err`, as the impl's
|
||||
/// methods are. A type no single name stands for (`()`, `(A, B)`, `[u8]`,
|
||||
/// `u32`) is written as the impl writes it. Mirrored byte-for-byte —
|
||||
/// change both.
|
||||
fn impl_item_owner(&self, node: Node) -> Option<String> {
|
||||
let list = node.parent().filter(|p| p.kind() == "declaration_list")?;
|
||||
let imp = list.parent().filter(|p| p.kind() == "impl_item")?;
|
||||
let mut ty = imp.child_by_field_name("type")?;
|
||||
while ty.kind() == "reference_type" {
|
||||
ty = ty.child_by_field_name("type")?;
|
||||
}
|
||||
self.impl_type_name(Some(ty)).or_else(|| {
|
||||
let words: Vec<&str> =
|
||||
self.text(ty).split([' ', '\t', '\r', '\n']).filter(|w| !w.is_empty()).collect();
|
||||
Some(words.join(" "))
|
||||
})
|
||||
}
|
||||
|
||||
// --- visitNode ------------------------------------------------------------
|
||||
|
||||
fn visit_node(&mut self, node: Node<'t>) {
|
||||
@@ -691,6 +711,9 @@ impl<'t> Walker<'t> {
|
||||
// Private unless `pub` (any form), as the wasm walker reads it:
|
||||
// the resolver keeps a private alias to its own module.
|
||||
visibility: Some(self.visibility_of(node)),
|
||||
// An impl's associated type is its type's (`ValueHint::Err`),
|
||||
// named only through a path, never bare.
|
||||
qualified_name: self.impl_item_owner(node).map(|o| format!("{o}::{name}")),
|
||||
..Extra::default()
|
||||
};
|
||||
self.create_node("type_alias", &name, node, extra);
|
||||
@@ -703,6 +726,8 @@ impl<'t> Walker<'t> {
|
||||
fn extract_variable(&mut self, node: Node<'t>) {
|
||||
let docstring = preceding_docstring(node, self.src);
|
||||
let name_field = node.child_by_field_name("name");
|
||||
// An impl's associated const is its type's (`Limits::MAX`), as its methods are.
|
||||
let owner = self.impl_item_owner(node);
|
||||
let mut declared: Option<(u32, String)> = None;
|
||||
for i in 0..node.named_child_count() {
|
||||
let Some(child) = node.named_child(i) else { continue };
|
||||
@@ -718,6 +743,7 @@ impl<'t> Walker<'t> {
|
||||
Extra {
|
||||
docstring: docstring.clone(),
|
||||
is_exported: Some(false),
|
||||
qualified_name: owner.as_ref().map(|o| format!("{o}::{name}")),
|
||||
..Extra::default()
|
||||
},
|
||||
);
|
||||
|
||||
@@ -71,6 +71,24 @@ export function rustImplTypeName(typeNode: SyntaxNode | null, source: string): s
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The type an item written directly in an `impl` block belongs to: `ValueHint`
|
||||
* for the `type Err = String;` of `impl FromStr for ValueHint`, which is then
|
||||
* `ValueHint::Err`, as the impl's methods are `ValueHint::…`. A type no single
|
||||
* name stands for (`()`, `(A, B)`, `[u8]`, `u32`) is written as the impl
|
||||
* writes it. Undefined for an item outside an impl. Mirrored byte-for-byte by
|
||||
* the native kernel's `impl_item_owner` (codegraph-kernel/src/rustlang.rs) —
|
||||
* change both.
|
||||
*/
|
||||
export function rustImplItemOwner(node: SyntaxNode, source: string): string | undefined {
|
||||
const impl = node.parent?.type === 'declaration_list' ? node.parent.parent : null;
|
||||
if (impl?.type !== 'impl_item') return undefined;
|
||||
let type = getChildByField(impl, 'type');
|
||||
while (type?.type === 'reference_type') type = getChildByField(type, 'type');
|
||||
if (!type) return undefined;
|
||||
return rustImplTypeName(type, source) ?? getNodeText(type, source).split(/[ \t\r\n]+/).filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export const rustExtractor: LanguageExtractor = {
|
||||
// `function_signature_item` is a trait method DECLARATION (`fn render(&self);`,
|
||||
// no body). Extracting it makes a trait's method set first-class, which
|
||||
|
||||
@@ -24,7 +24,7 @@ import { EXTRACTORS } from './languages';
|
||||
import { stripCppTemplateArgs, isCppConstructorDeclaration, cDeclaratorIdentifier, isClassLikeDefinition } from './languages/c-cpp';
|
||||
import { NestedIntervals, scanCppBraceScopes, type CppBraceScopes } from './languages/cpp-brace-scopes';
|
||||
import { appendCppMacroMethods } from './languages/cpp-macro-methods';
|
||||
import { rustImplTypeName } from './languages/rust';
|
||||
import { rustImplItemOwner, rustImplTypeName } from './languages/rust';
|
||||
import { goAliasTypeNames, goEmbeddedTypeName, goInterfaceMethodTypeNames, goStructFieldTypeNames } from './languages/go';
|
||||
import { dartMisparsedGenericCall, dartReceiverOf, isDartTypeName, pushDartTypeRefs } from './languages/dart';
|
||||
import { LiquidExtractor } from './liquid-extractor';
|
||||
@@ -3779,6 +3779,8 @@ export class TreeSitterExtractor {
|
||||
// Generic fallback for other languages
|
||||
// Try to find identifier children
|
||||
const nameField = getChildByField(node, 'name');
|
||||
// A Rust impl's associated const is its type's (`Limits::MAX`), as its methods are.
|
||||
const owner = this.language === 'rust' ? rustImplItemOwner(node, this.source) : undefined;
|
||||
let declared: Node | null = null;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
@@ -3791,6 +3793,7 @@ export class TreeSitterExtractor {
|
||||
const created = this.createNode(kind, name, child, {
|
||||
docstring,
|
||||
isExported,
|
||||
...(owner !== undefined ? { qualifiedName: `${owner}::${name}` } : {}),
|
||||
});
|
||||
if (created && nameField && child.startIndex === nameField.startIndex) {
|
||||
declared = created;
|
||||
@@ -3986,12 +3989,16 @@ export class TreeSitterExtractor {
|
||||
return true;
|
||||
}
|
||||
|
||||
const owner = this.language === 'rust' ? rustImplItemOwner(node, this.source) : undefined;
|
||||
const typeAliasNode = this.createNode('type_alias', name, node, {
|
||||
docstring,
|
||||
isExported,
|
||||
// A Rust alias is private to its module unless `pub` (any form): the
|
||||
// resolver keeps a private one to its own module (isRustNameInScope).
|
||||
...(this.language === 'rust' ? { visibility: this.extractor.getVisibility?.(node) } : {}),
|
||||
// An impl's associated type is its type's (`ValueHint::Err`), named only
|
||||
// through a path, never bare (isRustAssociatedItem).
|
||||
...(owner !== undefined ? { qualifiedName: `${owner}::${name}` } : {}),
|
||||
});
|
||||
|
||||
// Extract type references from the alias value (e.g., `type X = ITextModel | null`)
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
arrayElementType,
|
||||
tsImportedTypeFile,
|
||||
reExportSource,
|
||||
isRustAssociatedItem,
|
||||
} from './name-matcher';
|
||||
import { JS_ARRAY_METHODS } from './js-builtins';
|
||||
|
||||
@@ -2620,9 +2621,12 @@ function resolveRustPathReference(
|
||||
const file = resolveRustModuleFile(modSegs, ref.filePath, context);
|
||||
if (!file || file === ref.filePath) return null;
|
||||
|
||||
// A module's item, never an impl's associated one: serde's `use
|
||||
// crate::de::Error` is not `de/mod.rs`'s `impl SeqAccess`'s `type Error`.
|
||||
const target = context.getNodesInFile(file).find(
|
||||
(n) =>
|
||||
n.name === leaf &&
|
||||
!isRustAssociatedItem(n) &&
|
||||
(n.kind === 'function' ||
|
||||
n.kind === 'struct' ||
|
||||
n.kind === 'union' ||
|
||||
|
||||
@@ -8047,6 +8047,22 @@ function isRustItemVisibleFrom(candidate: Node, ref: UnresolvedRef): boolean {
|
||||
return candidate.filePath === ref.filePath || ref.filePath.startsWith(`${rustModuleDir(candidate.filePath)}/`);
|
||||
}
|
||||
|
||||
/** What a Rust `impl` declares besides its functions: `type Err = …;`, `const MAX: usize = …;`. */
|
||||
const RUST_ASSOCIATED_KINDS = new Set(['type_alias', 'variable', 'constant']);
|
||||
|
||||
/**
|
||||
* Whether a Rust item is an associated one: a `type` or `const` written in an
|
||||
* `impl` or a trait. It belongs to the type, not to the module, so Rust reaches
|
||||
* it only through a path — `Self::Err`, `<T as FromStr>::Err`, `T::Err` —
|
||||
* never by its name alone, and no module path (`std::error::Error`) or `use`
|
||||
* names it. The extractors qualify it by the impl's type (`ValueHint::Err`) or
|
||||
* its trait, as they do the methods; a module's own type alias or const is
|
||||
* qualified by nothing.
|
||||
*/
|
||||
export function isRustAssociatedItem(n: Node): boolean {
|
||||
return n.language === 'rust' && RUST_ASSOCIATED_KINDS.has(n.kind) && n.qualifiedName.includes('::');
|
||||
}
|
||||
|
||||
/** Does one of the file's globs bring in this candidate's module (or, for `use super::*`, its parent's)? */
|
||||
function rustGlobCovers(uses: RustUses, candidate: Node, ref: UnresolvedRef): boolean {
|
||||
if (uses.globs.has(rustModuleName(candidate.filePath))) return true;
|
||||
@@ -8105,6 +8121,10 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context:
|
||||
if (seg === 'Self') return candidate.kind === 'type_alias' && isInEnclosingRustImpl(candidate, ref, context);
|
||||
if (isRustGenericParam(seg, ref, context)) return false;
|
||||
}
|
||||
// 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 ((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 ||
|
||||
@@ -8115,6 +8135,10 @@ export function isRustNameInScope(candidate: Node, ref: UnresolvedRef, context:
|
||||
return rustModuleName(candidate.filePath) === seg || candidate.filePath.includes(`/${seg}/`) ||
|
||||
candidate.qualifiedName.split('::').includes(seg);
|
||||
}
|
||||
// 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
|
||||
// the `type Response` of `routing/mod.rs`'s `impl Service`.
|
||||
if (isRustAssociatedItem(candidate)) return false;
|
||||
if (candidate.kind === 'enum_member') {
|
||||
if (ref.referenceKind === 'references') return false;
|
||||
const uses = rustUsesOf(ref.filePath, context);
|
||||
|
||||
Reference in New Issue
Block a user