reference_query/lang/rust/
mod.rs1use 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 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" | "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 }
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 let qualified = qualify(parent, &name);
88 self.walk(child, Some(&qualified), true, out);
89 }
90 }
91 "mod_item" => {
92 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 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
149fn 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 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 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 assert_eq!(find(&syms, "get").parent.as_deref(), Some("outer::Store"));
243 }
244
245 #[test]
246 fn bare_module_declarations_are_not_indexed() {
247 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}