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