qlty_analysis/lang/
ruby.rs

1use crate::code::File;
2use crate::code::{child_source, node_source};
3use crate::lang::Language;
4use tree_sitter::Node;
5
6const CLASS_QUERY: &str = r#"
7[
8(class
9    name: [
10    (constant) @name
11    (scope_resolution
12        name: (_) @name)
13    ]) @definition.class
14(singleton_class
15    value: [
16    (constant) @name
17    (scope_resolution
18        name: (_) @name)
19    ]) @definition.class
20]
21"#;
22
23const FUNCTION_DECLARATION_QUERY: &str = r#"
24[
25    (method
26        name: (_) @name
27        parameters: (_)? @parameters)
28    (singleton_method
29        object: (_)
30        name: (_) @name
31        parameters: (_)? @parameters)
32] @definition.function
33"#;
34
35const FIELD_QUERY: &str = r#"
36[
37    (call
38        method: (identifier) @accessor_name
39        arguments: (argument_list (simple_symbol) @name)
40    (#eq? @accessor_name "attr_accessor"))
41
42    (call
43        method: (identifier) @writer_name
44        arguments: (argument_list (simple_symbol) @name)
45    (#eq? @writer_name "attr_writer"))
46
47    (call
48        method: (identifier) @reader_name
49        arguments: (argument_list (simple_symbol) @name)
50    (#eq? @reader_name "attr_reader"))
51
52    (call
53        receiver: (constant) @receiver_name
54        method: (identifier) @method_name
55        arguments: (argument_list (simple_symbol) @name))
56    (instance_variable) @name
57    (class_variable) @name
58] @field
59"#;
60
61pub struct Ruby {
62    pub class_query: tree_sitter::Query,
63    pub function_declaration_query: tree_sitter::Query,
64    pub field_query: tree_sitter::Query,
65}
66
67impl Ruby {
68    pub const BEGIN: &'static str = "begin";
69    pub const BINARY: &'static str = "binary";
70    pub const BLOCK: &'static str = "block";
71    pub const BREAK: &'static str = "break";
72    pub const CALL: &'static str = "call";
73    pub const CASE: &'static str = "case";
74    pub const CLASS_VARIABLE: &'static str = "class_variable";
75    pub const COMMENT: &'static str = "comment";
76    pub const CONDITIONAL: &'static str = "conditional";
77    pub const CONSTANT: &'static str = "constant";
78    pub const DO_BLOCK: &'static str = "do_block";
79    pub const ELSE: &'static str = "else";
80    pub const ELSIF: &'static str = "elsif";
81    pub const FOR: &'static str = "for";
82    pub const GLOBAL_VARIABLE: &'static str = "global_variable";
83    pub const IDENTIFIER: &'static str = "identifier";
84    pub const IF: &'static str = "if";
85    pub const INITIALIZE: &'static str = "initialize";
86    pub const INSTANCE_VARIABLE: &'static str = "instance_variable";
87    pub const METHOD_CALL: &'static str = "method_call";
88    pub const METHOD: &'static str = "method";
89    pub const NEXT: &'static str = "next";
90    pub const OPERATOR_ASSIGNMENT: &'static str = "operator_assignment";
91    pub const PROGRAM: &'static str = "program";
92    pub const RESCUE: &'static str = "rescue";
93    pub const RETURN: &'static str = "return";
94    pub const SELF: &'static str = "self";
95    pub const SINGLETON_METHOD: &'static str = "singleton_method";
96    pub const STRING: &'static str = "string";
97    pub const UNTIL: &'static str = "until";
98    pub const WHEN: &'static str = "when";
99    pub const WHILE: &'static str = "while";
100
101    pub const LOGICAL_AND: &'static str = "&&";
102    pub const LOGICAL_OR: &'static str = "||";
103    pub const AND: &'static str = "and";
104    pub const OR: &'static str = "or";
105}
106
107impl Default for Ruby {
108    fn default() -> Self {
109        let language = tree_sitter_ruby::language();
110
111        Self {
112            class_query: tree_sitter::Query::new(&language, CLASS_QUERY).unwrap(),
113            function_declaration_query: tree_sitter::Query::new(
114                &language,
115                FUNCTION_DECLARATION_QUERY,
116            )
117            .unwrap(),
118            field_query: tree_sitter::Query::new(&language, FIELD_QUERY).unwrap(),
119        }
120    }
121}
122
123impl Language for Ruby {
124    fn name(&self) -> &str {
125        "ruby"
126    }
127
128    fn self_keyword(&self) -> Option<&str> {
129        Some(Self::SELF)
130    }
131
132    fn class_query(&self) -> &tree_sitter::Query {
133        &self.class_query
134    }
135
136    fn function_declaration_query(&self) -> &tree_sitter::Query {
137        &self.function_declaration_query
138    }
139
140    fn field_query(&self) -> &tree_sitter::Query {
141        &self.field_query
142    }
143
144    fn invisible_container_nodes(&self) -> Vec<&str> {
145        vec![Self::PROGRAM]
146    }
147
148    fn if_nodes(&self) -> Vec<&str> {
149        vec![Self::IF]
150    }
151
152    fn elsif_nodes(&self) -> Vec<&str> {
153        vec![Self::ELSIF]
154    }
155
156    fn else_nodes(&self) -> Vec<&str> {
157        vec![Self::ELSE]
158    }
159
160    fn ternary_nodes(&self) -> Vec<&str> {
161        vec![Self::CONDITIONAL]
162    }
163
164    fn switch_nodes(&self) -> Vec<&str> {
165        vec![Self::CASE]
166    }
167
168    fn case_nodes(&self) -> Vec<&str> {
169        vec![Self::WHEN]
170    }
171
172    fn loop_nodes(&self) -> Vec<&str> {
173        vec![Self::WHILE, Self::UNTIL, Self::FOR]
174    }
175
176    fn except_nodes(&self) -> Vec<&str> {
177        vec![Self::RESCUE]
178    }
179
180    fn try_expression_nodes(&self) -> Vec<&str> {
181        vec![Self::BEGIN]
182    }
183
184    fn conditional_assignment_nodes(&self) -> Vec<&str> {
185        vec![Self::OPERATOR_ASSIGNMENT]
186    }
187
188    fn jump_nodes(&self) -> Vec<&str> {
189        vec![Self::BREAK, Self::NEXT]
190    }
191
192    fn return_nodes(&self) -> Vec<&str> {
193        vec![Self::RETURN]
194    }
195
196    fn binary_nodes(&self) -> Vec<&str> {
197        vec![Self::BINARY]
198    }
199
200    fn boolean_operator_nodes(&self) -> Vec<&str> {
201        vec![Self::LOGICAL_AND, Self::LOGICAL_OR, Self::AND, Self::OR]
202    }
203
204    fn field_nodes(&self) -> Vec<&str> {
205        vec![Self::INSTANCE_VARIABLE, Self::CLASS_VARIABLE]
206    }
207
208    fn call_nodes(&self) -> Vec<&str> {
209        vec![Self::CALL, Self::METHOD_CALL]
210    }
211
212    fn function_nodes(&self) -> Vec<&str> {
213        vec![Self::METHOD, Self::SINGLETON_METHOD]
214    }
215
216    fn closure_nodes(&self) -> Vec<&str> {
217        vec![Self::BLOCK, Self::DO_BLOCK]
218    }
219
220    fn comment_nodes(&self) -> Vec<&str> {
221        vec![Self::COMMENT]
222    }
223
224    fn string_nodes(&self) -> Vec<&str> {
225        vec![Self::STRING]
226    }
227
228    fn constructor_names(&self) -> Vec<&str> {
229        vec![Self::INITIALIZE]
230    }
231
232    fn iterator_method_identifiers(&self) -> Vec<&str> {
233        vec![
234            "each",
235            "map",
236            "collect",
237            "select",
238            "find_all",
239            "reject",
240            "find",
241            "detect",
242            "any?",
243            "all?",
244            "none?",
245            "one?",
246            "partition",
247            "group_by",
248            "each_with_index",
249            "reverse_each",
250            "each_entry",
251            "each_slice",
252            "each_cons",
253            "flat_map",
254            "collect_concat",
255            "zip",
256            "cycle",
257            "lazy",
258        ]
259    }
260
261    fn call_identifiers(&self, source_file: &File, node: &Node) -> (Option<String>, String) {
262        let function_kind = node.kind();
263
264        match function_kind {
265            Self::IDENTIFIER => (Some(Self::SELF.to_string()), node_source(node, source_file)),
266            Self::METHOD => {
267                let (receiver, object) = self.field_identifiers(source_file, node);
268
269                (Some(receiver), object)
270            }
271            Self::CALL => {
272                let receiver = if node.child_by_field_name("receiver").is_some() {
273                    child_source(node, "receiver", source_file)
274                } else {
275                    Self::SELF.to_string()
276                };
277
278                let method = if node.child_by_field_name("method").is_some() {
279                    child_source(node, "method", source_file)
280                } else if node.child_by_field_name("arguments").is_some() {
281                    "call".to_string()
282                } else {
283                    "<UNKNOWN>".to_string()
284                };
285
286                (Some(receiver), method)
287            }
288            _ => (Some("<UNKNOWN>".to_string()), "<UNKNOWN>".to_string()),
289        }
290    }
291
292    fn field_identifiers(&self, source_file: &File, node: &Node) -> (String, String) {
293        match node.kind() {
294            Self::CLASS_VARIABLE | Self::CONSTANT | Self::GLOBAL_VARIABLE => {
295                (Self::SELF.to_string(), node_source(node, source_file))
296            }
297            Self::INSTANCE_VARIABLE => {
298                let source = node_source(node, source_file);
299                let modified_source = source.strip_prefix('@').unwrap_or(&source);
300                (Self::SELF.to_string(), modified_source.to_string())
301            }
302            Self::METHOD => {
303                let method_node = node.child_by_field_name("method").unwrap();
304                (
305                    Self::SELF.to_string(),
306                    node_source(&method_node, source_file),
307                )
308            }
309            Self::CALL => (
310                child_source(node, "receiver", source_file),
311                child_source(node, "method", source_file),
312            ),
313            _ => (node.kind().to_string(), "<UNKNOWN>".to_string()),
314        }
315    }
316
317    fn tree_sitter_language(&self) -> tree_sitter::Language {
318        tree_sitter_ruby::language()
319    }
320}
321
322#[cfg(test)]
323
324mod test {
325    use super::*;
326    use std::collections::HashSet;
327    use tree_sitter::Tree;
328
329    #[test]
330    fn mutually_exclusive() {
331        let lang = Ruby::default();
332        let mut kinds: Vec<&str> = vec![];
333
334        kinds.extend(lang.invisible_container_nodes());
335        kinds.extend(lang.if_nodes());
336        kinds.extend(lang.else_nodes());
337        kinds.extend(lang.conditional_assignment_nodes());
338        kinds.extend(lang.switch_nodes());
339        kinds.extend(lang.case_nodes());
340        kinds.extend(lang.ternary_nodes());
341        kinds.extend(lang.loop_nodes());
342        kinds.extend(lang.except_nodes());
343        kinds.extend(lang.try_expression_nodes());
344        kinds.extend(lang.jump_nodes());
345        kinds.extend(lang.return_nodes());
346        kinds.extend(lang.binary_nodes());
347        kinds.extend(lang.field_nodes());
348        kinds.extend(lang.call_nodes());
349        kinds.extend(lang.function_nodes());
350        kinds.extend(lang.closure_nodes());
351        kinds.extend(lang.comment_nodes());
352        kinds.extend(lang.string_nodes());
353        kinds.extend(lang.boolean_operator_nodes());
354
355        let unique: HashSet<_> = kinds.iter().cloned().collect();
356        assert_eq!(unique.len(), kinds.len());
357    }
358
359    #[test]
360    fn field_identifier_read() {
361        let source_file = File::from_string("ruby", "self.foo");
362        let tree = source_file.parse();
363        let root_node = tree.root_node();
364        let expression = root_node.named_child(0).unwrap();
365        let language = Ruby::default();
366
367        assert_eq!(
368            language.field_identifiers(&source_file, &expression),
369            ("self".to_string(), "foo".to_string())
370        );
371    }
372
373    #[test]
374    fn field_identifier_write() {
375        let source_file = File::from_string("ruby", "self.foo = 1");
376        let tree = source_file.parse();
377        let root_node = tree.root_node();
378        let assignment = root_node.named_child(0).unwrap();
379        let field = assignment.child(0).unwrap();
380        let language = Ruby::default();
381
382        assert_eq!(
383            language.field_identifiers(&source_file, &field),
384            ("self".to_string(), "foo".to_string())
385        );
386    }
387
388    #[test]
389    fn field_identifier_collaborator() {
390        let source_file = File::from_string("ruby", "other.foo");
391        let tree = source_file.parse();
392        let root_node = tree.root_node();
393        let expression = root_node.named_child(0).unwrap();
394        let language = Ruby::default();
395
396        assert_eq!(
397            language.field_identifiers(&source_file, &expression),
398            ("other".to_string(), "foo".to_string())
399        );
400    }
401
402    #[test]
403    fn field_identifier_class_variables() {
404        let source_file = File::from_string("ruby", "@@foo");
405        let tree = source_file.parse();
406        let root_node = tree.root_node();
407        let expression = root_node.named_child(0).unwrap();
408        let language = Ruby::default();
409
410        assert_eq!(
411            language.field_identifiers(&source_file, &expression),
412            ("self".to_string(), "@@foo".to_string())
413        );
414    }
415
416    #[test]
417    fn field_identifier_instance_variables() {
418        let source_file = File::from_string("ruby", "@foo");
419        let tree = source_file.parse();
420        let root_node = tree.root_node();
421        let expression = root_node.named_child(0).unwrap();
422        let language = Ruby::default();
423
424        assert_eq!(
425            language.field_identifiers(&source_file, &expression),
426            ("self".to_string(), "foo".to_string())
427        );
428    }
429
430    #[test]
431    fn field_identifier_constant() {
432        let source_file = File::from_string("ruby", "MY_CONSTANT = 42");
433        let tree = source_file.parse();
434        let root_node = tree.root_node();
435        let expression = root_node.named_child(0).unwrap();
436        let field = expression.child(0).unwrap();
437        let language = Ruby::default();
438
439        assert_eq!(
440            language.field_identifiers(&source_file, &field),
441            ("self".to_string(), "MY_CONSTANT".to_string())
442        );
443    }
444
445    #[test]
446    fn field_identifier_global_variable() {
447        let source_file = File::from_string("ruby", "$global_var = 100");
448        let tree = source_file.parse();
449        let root_node = tree.root_node();
450        let expression = root_node.named_child(0).unwrap();
451        let field = expression.child(0).unwrap();
452        let language = Ruby::default();
453
454        assert_eq!(
455            language.field_identifiers(&source_file, &field),
456            ("self".to_string(), "$global_var".to_string())
457        );
458    }
459
460    #[test]
461    fn call_identifier() {
462        let source_file = File::from_string("ruby", "foo()");
463        let tree = source_file.parse();
464        let call = tree.root_node().child(0).unwrap();
465        let language = Ruby::default();
466
467        assert_eq!(
468            language.call_identifiers(&source_file, &call),
469            (Some("self".to_string()), "foo".to_string())
470        );
471    }
472
473    #[test]
474    fn call_identifier_with_module() {
475        let source_file = File::from_string("ruby", "Module::Class.method()");
476        let tree = source_file.parse();
477        let call = call_node(&tree);
478        let language = Ruby::default();
479
480        assert_eq!(
481            language.call_identifiers(&source_file, &call),
482            (Some("Module::Class".into()), "method".into())
483        );
484    }
485
486    #[test]
487    fn call_identifier_with_syntax_sugar() {
488        let source_file = File::from_string("ruby", "recognize_path.('some_path')");
489        let tree = source_file.parse();
490        let call = call_node(&tree);
491        let language = Ruby::default();
492
493        assert_eq!(
494            language.call_identifiers(&source_file, &call),
495            (Some("recognize_path".into()), "call".into())
496        );
497    }
498
499    #[test]
500    fn call_identifier_with_send() {
501        let source_file = File::from_string("ruby", "object.send(:method_name)");
502        let tree = source_file.parse();
503        let call = call_node(&tree);
504        let language = Ruby::default();
505
506        assert_eq!(
507            language.call_identifiers(&source_file, &call),
508            (Some("object".into()), "send".into())
509        );
510    }
511
512    #[test]
513    fn call_identifier_with_safe_navigation_operator() {
514        let source_file = File::from_string("ruby", "object&.method()");
515        let tree = source_file.parse();
516        let call = call_node(&tree);
517        let language = Ruby::default();
518
519        assert_eq!(
520            language.call_identifiers(&source_file, &call),
521            (Some("object".into()), "method".into())
522        );
523    }
524
525    #[test]
526    fn call_with_splat() {
527        let source_file = File::from_string("ruby", "foo(*args)");
528        let tree = source_file.parse();
529        let call = call_node(&tree);
530        let language = Ruby::default();
531
532        assert_eq!(
533            language.call_identifiers(&source_file, &call),
534            (Some("self".into()), "foo".into())
535        );
536    }
537
538    #[test]
539    fn tap_method() {
540        let source_file = File::from_string("ruby", "some_object.tap { |x| puts x }");
541        let tree = source_file.parse();
542        let call = call_node(&tree);
543        let language = Ruby::default();
544
545        assert_eq!(
546            language.call_identifiers(&source_file, &call),
547            (Some("some_object".into()), "tap".into())
548        );
549    }
550
551    #[test]
552    fn call_member() {
553        let source_file = File::from_string("ruby", "foo.bar()");
554        let tree = source_file.parse();
555        let call = call_node(&tree);
556        let language = Ruby::default();
557
558        assert_eq!(
559            language.call_identifiers(&source_file, &call),
560            (Some("foo".into()), "bar".into())
561        );
562    }
563
564    fn call_node(tree: &Tree) -> Node {
565        let root_node = tree.root_node();
566        let expression = root_node.named_child(0).unwrap();
567        return expression;
568    }
569}