Files
codegraph/__tests__/rust-associated-item-scope.test.ts
T
Colby MchenryandClaude Opus 5.5 b5553e238a 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>
2026-10-20 16:57:03 +00:00

229 lines
8.6 KiB
TypeScript

/**
* 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']);
});
});