reference_query/lang/rust/
mod.rs1use 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
38fn 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" | "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 }
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 let qualified = qualify(parent, &name, "::");
72 walk(ctx, child, Some(&qualified), out);
73 }
74 }
75 "mod_item" => {
76 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 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
108fn 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
115fn 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
132fn 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
143fn 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 let build = find(&syms, "build");
201 assert_eq!(build.kind, Kind::Function);
202 assert_eq!(build.parent, None);
203
204 let new = find(&syms, "new");
206 assert_eq!(new.kind, Kind::Function);
207 assert_eq!(new.parent.as_deref(), Some("Widget"));
208
209 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 assert_eq!(find(&syms, "get").parent.as_deref(), Some("outer::Store"));
238 }
239
240 #[test]
241 fn bare_module_declarations_are_not_indexed() {
242 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}