reference_query/lang/ruby/
mod.rs1use tree_sitter::{Node, Parser};
8
9use crate::core::{Kind, Symbol};
10use crate::lang::LanguagePlugin;
11
12const LANGUAGE: &str = "ruby";
13
14pub struct Ruby;
15
16impl LanguagePlugin for Ruby {
17 fn language(&self) -> &'static str {
18 LANGUAGE
19 }
20
21 fn extensions(&self) -> &[&str] {
22 &["rb"]
23 }
24
25 fn extract(&self, file: &str, source: &str) -> Vec<Symbol> {
26 let mut parser = Parser::new();
27 if parser
28 .set_language(&tree_sitter_ruby::LANGUAGE.into())
29 .is_err()
30 {
31 return Vec::new();
32 }
33 let Some(tree) = parser.parse(source, None) else {
34 return Vec::new();
35 };
36
37 let mut out = Vec::new();
38 let ctx = Ctx {
39 src: source.as_bytes(),
40 file,
41 };
42 ctx.walk(tree.root_node(), None, &mut out);
43 out
44 }
45}
46
47struct Ctx<'a> {
48 src: &'a [u8],
49 file: &'a str,
50}
51
52impl Ctx<'_> {
53 fn walk(&self, node: Node, parent: Option<&str>, out: &mut Vec<Symbol>) {
55 let mut cursor = node.walk();
56 for child in node.children(&mut cursor) {
57 match child.kind() {
58 "class" | "module" => {
59 let kind = if child.kind() == "class" {
60 Kind::Class
61 } else {
62 Kind::Module
63 };
64 if let Some(name) = self.field_text(child, "name") {
65 let (leaf, prefix) = split_qualified(&name);
70 let effective_parent = match prefix {
71 Some(p) => Some(qualify(parent, p, "::")),
72 None => parent.map(str::to_string),
73 };
74 out.push(self.symbol(leaf, kind, child, effective_parent.as_deref()));
75 let qualified = qualify(effective_parent.as_deref(), leaf, "::");
76 self.walk(child, Some(&qualified), out);
77 } else {
78 self.walk(child, parent, out);
79 }
80 }
81 "method" | "singleton_method" => {
82 if let Some(name) = self.field_text(child, "name") {
83 out.push(self.symbol(&name, Kind::Method, child, parent));
84 }
85 }
87 _ => self.walk(child, parent, out),
88 }
89 }
90 }
91
92 fn field_text(&self, node: Node, field: &str) -> Option<String> {
93 node.child_by_field_name(field)
94 .and_then(|n| n.utf8_text(self.src).ok())
95 .map(str::to_string)
96 }
97
98 fn symbol(&self, name: &str, kind: Kind, node: Node, parent: Option<&str>) -> Symbol {
99 Symbol {
100 name: name.to_string(),
101 kind,
102 language: LANGUAGE.to_string(),
103 file: self.file.to_string(),
104 line: node.start_position().row as u32 + 1,
105 parent: parent.map(str::to_string),
106 }
107 }
108}
109
110fn split_qualified(name: &str) -> (&str, Option<&str>) {
114 match name.rfind("::") {
115 Some(i) => {
116 let prefix = &name[..i];
117 (&name[i + 2..], (!prefix.is_empty()).then_some(prefix))
118 }
119 None => (name, None),
120 }
121}
122
123fn qualify(parent: Option<&str>, name: &str, sep: &str) -> String {
124 match parent {
125 Some(p) => format!("{p}{sep}{name}"),
126 None => name.to_string(),
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn extract(source: &str) -> Vec<Symbol> {
135 Ruby.extract("test.rb", source)
136 }
137
138 fn find<'a>(syms: &'a [Symbol], name: &str) -> &'a Symbol {
139 syms.iter()
140 .find(|s| s.name == name)
141 .unwrap_or_else(|| panic!("no symbol named {name} in {syms:?}"))
142 }
143
144 #[test]
145 fn extracts_class_module_and_methods_with_nesting() {
146 let src = r#"
147module Billing
148 class RefundProcessor
149 def perform
150 end
151
152 def self.build
153 end
154 end
155end
156"#;
157 let syms = extract(src);
158
159 let module = find(&syms, "Billing");
160 assert_eq!(module.kind, Kind::Module);
161 assert_eq!(module.parent, None);
162 assert_eq!(module.line, 2);
163
164 let class = find(&syms, "RefundProcessor");
165 assert_eq!(class.kind, Kind::Class);
166 assert_eq!(class.parent.as_deref(), Some("Billing"));
167
168 let perform = find(&syms, "perform");
169 assert_eq!(perform.kind, Kind::Method);
170 assert_eq!(perform.parent.as_deref(), Some("Billing::RefundProcessor"));
171
172 let build = find(&syms, "build");
174 assert_eq!(build.kind, Kind::Method);
175 assert_eq!(build.parent.as_deref(), Some("Billing::RefundProcessor"));
176 }
177
178 #[test]
179 fn compact_namespace_is_split_into_leaf_and_parent() {
180 let src = "class My::Module::EmployeesController\n def index\n end\nend\n";
184 let syms = extract(src);
185
186 let class = find(&syms, "EmployeesController");
187 assert_eq!(class.kind, Kind::Class);
188 assert_eq!(class.parent.as_deref(), Some("My::Module"));
189
190 let index = find(&syms, "index");
191 assert_eq!(
192 index.parent.as_deref(),
193 Some("My::Module::EmployeesController")
194 );
195 }
196
197 #[test]
198 fn empty_and_unparseable_yield_no_symbols() {
199 assert!(extract("").is_empty());
200 assert!(extract("# just a comment\n").is_empty());
201 }
202
203 #[test]
204 fn language_tag_is_set() {
205 let syms = extract("class Foo\nend\n");
206 assert_eq!(syms[0].language, "ruby");
207 }
208}