Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions __tests__/fixtures/kernel-parity/torture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
}
Expand Down
228 changes: 228 additions & 0 deletions __tests__/rust-associated-item-scope.test.ts
Original file line number Diff line number Diff line change
@@ -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']);
});
});
26 changes: 26 additions & 0 deletions codegraph-kernel/src/rustlang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>) {
Expand Down Expand Up @@ -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);
Expand All @@ -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 };
Expand All @@ -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()
},
);
Expand Down
18 changes: 18 additions & 0 deletions src/extraction/languages/rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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`)
Expand Down
Loading