Skip to main content

reference_query/lang/rust/
mod.rs

1//! Rust plugin — the second language, and what rq dogfoods on its own source.
2//!
3//! Extracts the definitions you navigate to: `fn` (free → function, inside an
4//! `impl`/`trait` → method), `struct`, `enum`, `trait`, and `mod`. `parent`
5//! carries the enclosing qualified name (`::`-joined) so a method renders as
6//! `bar · Foo` and a nested type as `outer · mod`. `impl` blocks aren't symbols
7//! themselves; they just supply the parent for the methods inside them.
8
9use tree_sitter::Node;
10
11use crate::core::{Kind, Symbol};
12use crate::lang::{Ctx, LanguagePlugin, extract_with, qualify};
13
14const LANGUAGE: &str = "rust";
15
16pub struct Rust;
17
18impl LanguagePlugin for Rust {
19    fn language(&self) -> &'static str {
20        LANGUAGE
21    }
22
23    fn extensions(&self) -> &[&str] {
24        &["rs"]
25    }
26
27    fn extract(&self, file: &str, source: &str) -> Vec<Symbol> {
28        extract_with(
29            LANGUAGE,
30            tree_sitter_rust::LANGUAGE.into(),
31            file,
32            source,
33            |ctx, root, out| walk(ctx, root, None, out),
34        )
35    }
36}
37
38/// Recursively collect definitions. `parent` is the enclosing qualified name.
39fn walk(ctx: &Ctx, node: Node, parent: Option<&str>, out: &mut Vec<Symbol>) {
40    let mut cursor = node.walk();
41    for child in node.children(&mut cursor) {
42        match child.kind() {
43            // `function_item` has a body; `function_signature_item` is a
44            // bodyless signature (a trait method declaration). A `self`
45            // receiver makes it a method; without one it's a function —
46            // including an associated fn like `Widget::new()`.
47            "function_item" | "function_signature_item" => {
48                if let Some(name) = ctx.field_text(child, "name") {
49                    let kind = if has_self(child) {
50                        Kind::Method
51                    } else {
52                        Kind::Function
53                    };
54                    push(ctx, out, &name, kind, child, parent);
55                }
56                // bodies rarely hold further definitions worth surfacing
57            }
58            "struct_item" | "enum_item" | "union_item" => {
59                if let Some(name) = ctx.field_text(child, "name") {
60                    let kind = match child.kind() {
61                        "enum_item" => Kind::Enum,
62                        _ => Kind::Struct,
63                    };
64                    push(ctx, out, &name, kind, child, parent);
65                }
66            }
67            "trait_item" => {
68                if let Some(name) = ctx.field_text(child, "name") {
69                    push(ctx, out, &name, Kind::Trait, child, parent);
70                    // trait method signatures are methods of the trait
71                    let qualified = qualify(parent, &name, "::");
72                    walk(ctx, child, Some(&qualified), out);
73                }
74            }
75            "mod_item" => {
76                // Only a module *with a body* is a definition worth surfacing.
77                // A bare `mod x;` is just a re-export pointer to another file —
78                // indexing it competes with (and can outrank) the real
79                // definitions it forwards to.
80                if child.child_by_field_name("body").is_some()
81                    && let Some(name) = ctx.field_text(child, "name")
82                {
83                    push(ctx, out, &name, Kind::Module, child, parent);
84                    let qualified = qualify(parent, &name, "::");
85                    walk(ctx, child, Some(&qualified), out);
86                }
87            }
88            "impl_item" => {
89                // an impl isn't a symbol; its `type` becomes the parent of the
90                // methods inside it
91                let ty = ctx.field_text(child, "type").map(|t| base_type(&t));
92                let qualified = match &ty {
93                    Some(t) => qualify(parent, t, "::"),
94                    None => parent.map(str::to_string).unwrap_or_default(),
95                };
96                let p = if qualified.is_empty() {
97                    None
98                } else {
99                    Some(qualified.as_str())
100                };
101                walk(ctx, child, p, out);
102            }
103            _ => walk(ctx, child, parent, out),
104        }
105    }
106}
107
108/// Emit a symbol carrying the item's declared visibility.
109fn push(ctx: &Ctx, out: &mut Vec<Symbol>, name: &str, kind: Kind, node: Node, p: Option<&str>) {
110    let mut s = ctx.symbol(name, kind, node, p);
111    s.visibility = Some(visibility(ctx, node));
112    out.push(s);
113}
114
115/// The item's declared visibility: `pub` → public, any scoped `pub(...)` →
116/// crate, none → private (Rust's default).
117fn visibility(ctx: &Ctx, node: Node) -> &'static str {
118    let mut cursor = node.walk();
119    for child in node.children(&mut cursor) {
120        if child.kind() == "visibility_modifier" {
121            let text = ctx.node_text(child).unwrap_or_default();
122            return if text.contains('(') {
123                "crate"
124            } else {
125                "public"
126            };
127        }
128    }
129    "private"
130}
131
132/// Whether an fn declares a `self` receiver (an instance method).
133fn has_self(node: Node) -> bool {
134    node.child_by_field_name("parameters")
135        .is_some_and(|params| {
136            let mut cursor = params.walk();
137            params
138                .children(&mut cursor)
139                .any(|p| p.kind() == "self_parameter")
140        })
141}
142
143/// The base type name from an impl's `type` field, dropping any generic
144/// arguments and path qualifier: `Foo<T>` → `Foo`, `a::b::Foo` → `Foo`.
145fn base_type(ty: &str) -> String {
146    let head = ty.split('<').next().unwrap_or(ty).trim();
147    head.rsplit("::").next().unwrap_or(head).trim().to_string()
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    fn extract(source: &str) -> Vec<Symbol> {
155        Rust.extract("test.rs", source)
156    }
157
158    fn find<'a>(syms: &'a [Symbol], name: &str) -> &'a Symbol {
159        syms.iter()
160            .find(|s| s.name == name)
161            .unwrap_or_else(|| panic!("no symbol named {name} in {syms:?}"))
162    }
163
164    #[test]
165    fn extracts_types_functions_and_impl_methods() {
166        let src = r#"
167pub struct Widget {
168    size: u32,
169}
170
171pub enum Color {
172    Red,
173    Green,
174}
175
176pub trait Render {
177    fn render(&self) -> String;
178}
179
180impl Widget {
181    pub fn new() -> Self {
182        Widget { size: 0 }
183    }
184}
185
186pub fn build() -> Widget {
187    Widget::new()
188}
189"#;
190        let syms = extract(src);
191
192        let widget = find(&syms, "Widget");
193        assert_eq!(widget.kind, Kind::Struct);
194        assert_eq!(widget.parent, None);
195
196        assert_eq!(find(&syms, "Color").kind, Kind::Enum);
197        assert_eq!(find(&syms, "Render").kind, Kind::Trait);
198
199        // a free fn is a function; an fn with a self receiver is a method
200        let build = find(&syms, "build");
201        assert_eq!(build.kind, Kind::Function);
202        assert_eq!(build.parent, None);
203
204        // an associated fn (no self) is a *function* of the type, not a method
205        let new = find(&syms, "new");
206        assert_eq!(new.kind, Kind::Function);
207        assert_eq!(new.parent.as_deref(), Some("Widget"));
208
209        // a trait method signature is a method of the trait
210        let render = find(&syms, "render");
211        assert_eq!(render.kind, Kind::Method);
212        assert_eq!(render.parent.as_deref(), Some("Render"));
213
214        assert_eq!(widget.language, "rust");
215    }
216
217    #[test]
218    fn qualifies_through_modules_and_generic_impls() {
219        let src = r#"
220mod outer {
221    pub struct Store<T> {
222        inner: T,
223    }
224
225    impl<T> Store<T> {
226        pub fn get(&self) -> &T {
227            &self.inner
228        }
229    }
230}
231"#;
232        let syms = extract(src);
233
234        assert_eq!(find(&syms, "outer").kind, Kind::Module);
235        assert_eq!(find(&syms, "Store").parent.as_deref(), Some("outer"));
236        // generic args and the module path resolve to the bare type name
237        assert_eq!(find(&syms, "get").parent.as_deref(), Some("outer::Store"));
238    }
239
240    #[test]
241    fn bare_module_declarations_are_not_indexed() {
242        // `mod foo;` is a re-export pointer, not a definition; only a module with
243        // a body is surfaced.
244        let syms = extract("mod search;\nmod handler { pub fn run() {} }\n");
245        assert!(
246            !syms.iter().any(|s| s.name == "search"),
247            "bare `mod search;` should be skipped: {syms:?}"
248        );
249        assert_eq!(find(&syms, "handler").kind, Kind::Module);
250        assert_eq!(find(&syms, "run").kind, Kind::Function);
251    }
252
253    #[test]
254    fn empty_and_unparseable_yield_no_symbols() {
255        assert!(extract("").is_empty());
256        assert!(extract("// just a comment\n").is_empty());
257    }
258
259    #[test]
260    fn visibility_reflects_the_pub_modifier() {
261        let src = "pub fn open() {}\npub(crate) fn shared() {}\nfn helper() {}\n";
262        let syms = extract(src);
263        assert_eq!(find(&syms, "open").visibility, Some("public"));
264        assert_eq!(find(&syms, "shared").visibility, Some("crate"));
265        assert_eq!(find(&syms, "helper").visibility, Some("private"));
266    }
267}