1mod relations;
18
19pub use relations::RubyGraphBuilder;
20
21use sqry_core::ast::{Scope, ScopeId, link_nested_scopes};
22use sqry_core::plugin::{
23 LanguageMetadata, LanguagePlugin,
24 error::{ParseError, ScopeError},
25};
26use std::path::Path;
27use streaming_iterator::StreamingIterator;
28use tree_sitter::{Language, Parser, Query, QueryCursor, Tree};
29
30pub struct RubyPlugin {
53 graph_builder: RubyGraphBuilder,
54}
55
56impl RubyPlugin {
57 #[must_use]
58 pub fn new() -> Self {
59 Self {
60 graph_builder: RubyGraphBuilder::default(),
61 }
62 }
63}
64
65impl Default for RubyPlugin {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl LanguagePlugin for RubyPlugin {
72 fn metadata(&self) -> LanguageMetadata {
73 LanguageMetadata {
74 id: "ruby",
75 name: "Ruby",
76 version: env!("CARGO_PKG_VERSION"),
77 author: "Verivus Pty Ltd",
78 description: "Ruby language support for sqry",
79 tree_sitter_version: "0.25",
80 }
81 }
82
83 fn extensions(&self) -> &'static [&'static str] {
84 &["rb", "rake", "gemspec"]
85 }
86
87 fn language(&self) -> Language {
88 tree_sitter_ruby::LANGUAGE.into()
89 }
90
91 fn parse_ast(&self, content: &[u8]) -> Result<Tree, ParseError> {
92 let mut parser = Parser::new();
93 let language = self.language();
94
95 parser.set_language(&language).map_err(|e| {
96 ParseError::LanguageSetFailed(format!("Failed to set Ruby language: {e}"))
97 })?;
98
99 parser
100 .parse(content, None)
101 .ok_or(ParseError::TreeSitterFailed)
102 }
103
104 fn extract_scopes(
105 &self,
106 tree: &Tree,
107 content: &[u8],
108 file_path: &Path,
109 ) -> Result<Vec<Scope>, ScopeError> {
110 Self::extract_ruby_scopes(tree, content, file_path)
111 }
112
113 fn graph_builder(&self) -> Option<&dyn sqry_core::graph::GraphBuilder> {
114 Some(&self.graph_builder)
115 }
116}
117
118impl RubyPlugin {
119 fn extract_ruby_scopes(
121 tree: &Tree,
122 content: &[u8],
123 file_path: &Path,
124 ) -> Result<Vec<Scope>, ScopeError> {
125 let root_node = tree.root_node();
126 let language = tree_sitter_ruby::LANGUAGE.into();
127
128 let scope_query = Self::scope_query_source();
129
130 let query = Query::new(&language, scope_query)
131 .map_err(|e| ScopeError::QueryCompilationFailed(e.to_string()))?;
132
133 let mut scopes = Vec::new();
134 let mut cursor = QueryCursor::new();
135 let mut query_matches = cursor.matches(&query, root_node, content);
136
137 while let Some(m) = query_matches.next() {
138 let mut scope_type = None;
139 let mut scope_name = None;
140 let mut scope_start = None;
141 let mut scope_end = None;
142
143 for capture in m.captures {
144 let capture_name = query.capture_names()[capture.index as usize];
145 let node = capture.node;
146
147 let capture_ext = std::path::Path::new(capture_name)
148 .extension()
149 .and_then(|ext| ext.to_str());
150
151 if capture_ext.is_some_and(|ext| ext.eq_ignore_ascii_case("type")) {
152 scope_type = Some(capture_name.trim_end_matches(".type").to_string());
153 scope_start = Some(node.start_position());
154 scope_end = Some(node.end_position());
155 } else if capture_ext.is_some_and(|ext| ext.eq_ignore_ascii_case("name")) {
156 scope_name = node
157 .utf8_text(content)
158 .ok()
159 .map(std::string::ToString::to_string);
160 }
161 }
162
163 if let (Some(stype), Some(sname), Some(start), Some(end)) =
164 (scope_type, scope_name, scope_start, scope_end)
165 {
166 let scope = Scope {
167 id: ScopeId::new(0),
168 scope_type: stype,
169 name: sname,
170 file_path: file_path.to_path_buf(),
171 start_line: start.row + 1,
172 start_column: start.column,
173 end_line: end.row + 1,
174 end_column: end.column,
175 parent_id: None,
176 };
177 scopes.push(scope);
178 }
179 }
180
181 scopes.sort_by_key(|s| (s.start_line, s.start_column));
182
183 link_nested_scopes(&mut scopes);
184
185 Ok(scopes)
186 }
187
188 fn scope_query_source() -> &'static str {
190 r"
191; Method scopes
192(method
193 name: (identifier) @method.name
194) @method.type
195
196; Singleton method scopes
197(singleton_method
198 name: (identifier) @singleton_method.name
199) @singleton_method.type
200
201; Class scopes
202(class
203 name: (constant) @class.name
204) @class.type
205
206; Module scopes
207(module
208 name: (constant) @module.name
209) @module.type
210"
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_metadata() {
220 let plugin = RubyPlugin::default();
221 let metadata = plugin.metadata();
222
223 assert_eq!(metadata.id, "ruby");
224 assert_eq!(metadata.name, "Ruby");
225 assert_eq!(metadata.author, "Verivus Pty Ltd");
226 }
227
228 #[test]
229 fn test_extensions() {
230 let plugin = RubyPlugin::default();
231 let extensions = plugin.extensions();
232
233 assert_eq!(extensions.len(), 3);
234 assert!(extensions.contains(&"rb"));
235 assert!(extensions.contains(&"rake"));
236 assert!(extensions.contains(&"gemspec"));
237 }
238
239 #[test]
240 fn test_language() {
241 let plugin = RubyPlugin::default();
242 let language = plugin.language();
243
244 assert!(language.abi_version() > 0);
245 }
246
247 #[test]
248 fn test_parse_ast_simple() {
249 let plugin = RubyPlugin::default();
250 let source = b"def hello; end";
251
252 let tree = plugin.parse_ast(source).unwrap();
253 assert!(!tree.root_node().has_error());
254 }
255}