Files
codegraph/__tests__/rust-assoc-qualified-self.test.ts
T
Colby MchenryandClaude Opus 5.5 818e780336 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>
2026-10-20 18:11:39 +00:00

119 lines
4.4 KiB
TypeScript

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