From 9a453488a913b0d258744290216144917cfbcf24 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Mon, 19 Oct 2026 23:54:12 -0500 Subject: [PATCH] fix(rust): an impl's associated type or const no longer takes a bare name or a module path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `type` or `const` written inside an `impl` belongs to the type: Rust reaches it only through a path like `Self::Err`, `::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 --- CHANGELOG.md | 1 + __tests__/fixtures/kernel-parity/torture.rs | 16 ++ __tests__/rust-associated-item-scope.test.ts | 228 +++++++++++++++++++ codegraph-kernel/src/rustlang.rs | 26 +++ src/extraction/languages/rust.ts | 18 ++ src/extraction/tree-sitter.ts | 9 +- src/resolution/import-resolver.ts | 4 + src/resolution/name-matcher.ts | 24 ++ 8 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 __tests__/rust-associated-item-scope.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index db92767b17..306417b192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -214,6 +214,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Rust, a call written `Self::new(…)`, `Self::from_std(…)` or `Self::Text(…)` now links to that function or variant of the type whose `impl` it is written in, even when other types have one of the same name. Before, such a call was linked only when nothing else in the project shared the name, so most `Self::new(…)` constructors and many helper calls were missing from callers and impact. Re-index Rust projects after upgrading. - In Rust, a call like `String::from("…")`, `Vec::from([…])` or `NonNull::from(x)` no longer links to your project's own `impl From 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 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 diff --git a/__tests__/fixtures/kernel-parity/torture.rs b/__tests__/fixtures/kernel-parity/torture.rs index 63fe390a16..86f04ff6d6 100644 --- a/__tests__/fixtures/kernel-parity/torture.rs +++ b/__tests__/fixtures/kernel-parity/torture.rs @@ -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) {} } diff --git a/__tests__/rust-associated-item-scope.test.ts b/__tests__/rust-associated-item-scope.test.ts new file mode 100644 index 0000000000..bb0066d7a2 --- /dev/null +++ b/__tests__/rust-associated-item-scope.test.ts @@ -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`, `::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 = { + '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 = http::Response;\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 tower::Service> for Router {', + ' type Response = Response;', + ' type Error = Infallible;', + '', + ' fn call(&mut self, req: http::Request) -> Result {', + ' 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 tower::Service> for Route {', + ' type Response = Response;', + '', + ' fn call(&mut self, req: http::Request) -> 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 {', + ' match s {', + ' "other" => Ok(ValueHint::Other),', + ' _ => Err(format!("unknown hint: {s}")),', + ' }', + ' }', + '}', + '', + 'pub fn parse(s: &str) -> Result {', + ' 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 + \'_ {', + ' JarIter', + ' }', + '}', + '', + 'impl Iterator for JarIter {', + ' type Item = String;', + '', + ' fn next(&mut self) -> Option {', + ' 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` 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']); + }); +}); diff --git a/codegraph-kernel/src/rustlang.rs b/codegraph-kernel/src/rustlang.rs index 089d6a7584..dd57b790e8 100644 --- a/codegraph-kernel/src/rustlang.rs +++ b/codegraph-kernel/src/rustlang.rs @@ -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 { + 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() }, ); diff --git a/src/extraction/languages/rust.ts b/src/extraction/languages/rust.ts index 9c4710022c..c336a79ab1 100644 --- a/src/extraction/languages/rust.ts +++ b/src/extraction/languages/rust.ts @@ -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 diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 27d222c321..66e1e8c491 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -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`) diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index 8ab161c849..1951975cb5 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -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' || diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index c77621fb25..b28f925674 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -7803,6 +7803,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`, `::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; @@ -7861,6 +7877,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, + // `::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 || @@ -7871,6 +7891,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);