Skip to main content

icb_parser/lang/
ruby.rs

1//! Ruby language parser using tree-sitter-ruby.
2//!
3//! Extracts method definitions, singleton methods, class/module definitions,
4//! call expressions, lambdas, and blocks as anonymous functions.
5
6use crate::facts::RawNode;
7use icb_common::{IcbError, Language, NodeKind};
8use tree_sitter::Parser;
9
10use super::common::{child_of_kind, traverse_node};
11
12/// Parse Ruby source code and return the extracted facts.
13pub fn parse_ruby(source: &str) -> Result<Vec<RawNode>, IcbError> {
14    let mut parser = Parser::new();
15    parser
16        .set_language(&tree_sitter_ruby::language())
17        .map_err(|e| IcbError::Parse(format!("cannot set tree-sitter-ruby language: {e}")))?;
18
19    let tree = parser
20        .parse(source, None)
21        .ok_or_else(|| IcbError::Parse("tree-sitter parse returned None for Ruby source".into()))?;
22
23    let mut facts = Vec::new();
24
25    let classifier = |node: &tree_sitter::Node,
26                      source: &str|
27     -> Option<(NodeKind, Option<String>, bool)> {
28        match node.kind() {
29            "method" | "singleton_method" => {
30                let name = child_of_kind(*node, "identifier")
31                    .and_then(|n| n.utf8_text(source.as_bytes()).ok())
32                    .map(|s| s.to_string());
33                Some((NodeKind::Function, name, true))
34            }
35            "class" | "module" => {
36                let name = child_of_kind(*node, "constant")
37                    .and_then(|n| n.utf8_text(source.as_bytes()).ok())
38                    .map(|s| s.to_string());
39                Some((NodeKind::Class, name, true))
40            }
41            "call" => {
42                let name_node = child_of_kind(*node, "identifier")
43                    .or_else(|| child_of_kind(*node, "constant"))
44                    .or_else(|| child_of_kind(*node, "method_identifier"));
45                let name = name_node
46                    .and_then(|n| n.utf8_text(source.as_bytes()).ok())
47                    .map(|s| s.to_string());
48                Some((NodeKind::CallSite, name, false))
49            }
50            "lambda" => Some((NodeKind::Function, Some("lambda".into()), true)),
51            "do_block" | "brace_block" => Some((NodeKind::Function, Some("block".into()), true)),
52            _ => None,
53        }
54    };
55
56    traverse_node(
57        tree.root_node(),
58        source,
59        &mut facts,
60        None,
61        Language::Ruby,
62        &classifier,
63    );
64    Ok(facts)
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use icb_common::NodeKind;
71
72    #[test]
73    fn test_simple_method() {
74        let code = "def foo; end";
75        let facts = parse_ruby(code).unwrap();
76        let funcs: Vec<_> = facts
77            .iter()
78            .filter(|n| n.kind == NodeKind::Function)
79            .collect();
80        assert_eq!(funcs.len(), 1);
81        assert_eq!(funcs[0].name.as_deref(), Some("foo"));
82    }
83
84    #[test]
85    fn test_class() {
86        let code = "class MyClass; end";
87        let facts = parse_ruby(code).unwrap();
88        let classes: Vec<_> = facts
89            .iter()
90            .filter(|n| n.kind == NodeKind::Class && n.name.as_deref() == Some("MyClass"))
91            .collect();
92        assert!(!classes.is_empty(), "expected class MyClass");
93    }
94
95    #[test]
96    fn test_call() {
97        let code = "puts 'hello'";
98        let facts = parse_ruby(code).unwrap();
99        let calls: Vec<_> = facts
100            .iter()
101            .filter(|n| n.kind == NodeKind::CallSite)
102            .collect();
103        assert_eq!(calls.len(), 1);
104        assert_eq!(calls[0].name.as_deref(), Some("puts"));
105    }
106
107    #[test]
108    fn test_lambda() {
109        let code = "-> { }";
110        let facts = parse_ruby(code).unwrap();
111        let lambdas: Vec<_> = facts
112            .iter()
113            .filter(|n| n.name.as_deref() == Some("lambda"))
114            .collect();
115        assert!(!lambdas.is_empty(), "expected at least one lambda");
116    }
117}