1use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6pub struct Ruby;
8
9impl Language for Ruby {
10 fn name(&self) -> &'static str {
11 "Ruby"
12 }
13 fn extensions(&self) -> &'static [&'static str] {
14 &["rb"]
15 }
16 fn grammar_name(&self) -> &'static str {
17 "ruby"
18 }
19
20 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21 Some(self)
22 }
23
24 fn signature_suffix(&self) -> &'static str {
25 "; end"
26 }
27
28 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
29 let mut doc_lines: Vec<String> = Vec::new();
30 let mut prev = node.prev_sibling();
31
32 while let Some(sibling) = prev {
33 if sibling.kind() == "comment" {
34 let text = &content[sibling.byte_range()];
35 if let Some(line) = text.strip_prefix('#') {
36 let line = line.strip_prefix(' ').unwrap_or(line);
37 doc_lines.push(line.to_string());
38 } else {
39 break;
40 }
41 } else {
42 break;
43 }
44 prev = sibling.prev_sibling();
45 }
46
47 if doc_lines.is_empty() {
48 return None;
49 }
50
51 doc_lines.reverse();
52 let joined = doc_lines.join("\n").trim().to_string();
53 if joined.is_empty() {
54 None
55 } else {
56 Some(joined)
57 }
58 }
59
60 fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
61 let mut implements = Vec::new();
62 let mut cursor = node.walk();
63 for child in node.children(&mut cursor) {
64 if child.kind() == "superclass" {
65 let mut sc = child.walk();
66 for t in child.children(&mut sc) {
67 if t.kind() == "constant" || t.kind() == "scope_resolution" {
68 implements.push(content[t.byte_range()].to_string());
69 }
70 }
71 }
72 }
73 crate::ImplementsInfo {
74 is_interface: false,
75 implements,
76 }
77 }
78
79 fn build_signature(&self, node: &Node, content: &str) -> String {
80 let name = match self.node_name(node, content) {
81 Some(n) => n,
82 None => {
83 return content[node.byte_range()]
84 .lines()
85 .next()
86 .unwrap_or("")
87 .trim()
88 .to_string();
89 }
90 };
91 match node.kind() {
92 "method" | "singleton_method" => format!("def {}", name),
93 "class" => format!("class {}", name),
94 "module" => format!("module {}", name),
95 _ => {
96 let text = &content[node.byte_range()];
97 text.lines().next().unwrap_or(text).trim().to_string()
98 }
99 }
100 }
101
102 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
103 if import.is_relative {
105 format!("require_relative '{}'", import.module)
106 } else {
107 format!("require '{}'", import.module)
108 }
109 }
110
111 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
112 let name = symbol.name.as_str();
113 match symbol.kind {
114 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
115 crate::SymbolKind::Module => name == "tests" || name == "test",
116 _ => false,
117 }
118 }
119
120 fn test_file_globs(&self) -> &'static [&'static str] {
121 &[
122 "**/spec/**/*.rb",
123 "**/test/**/*.rb",
124 "**/*_test.rb",
125 "**/*_spec.rb",
126 ]
127 }
128
129 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
130 let mut prev = node.prev_sibling();
134 while let Some(sibling) = prev {
135 if sibling.kind() == "call" || sibling.kind() == "identifier" {
136 let text = &content[sibling.byte_range()];
137 let method = text.split_whitespace().next().unwrap_or(text);
138 match method {
139 "private" => return Visibility::Private,
140 "protected" => return Visibility::Protected,
141 "public" => return Visibility::Public,
142 _ => {}
143 }
144 }
145 prev = sibling.prev_sibling();
146 }
147 Visibility::Public
149 }
150
151 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
152 node.child_by_field_name("body")
153 }
154 fn analyze_container_body(
155 &self,
156 body_node: &Node,
157 content: &str,
158 inner_indent: &str,
159 ) -> Option<ContainerBody> {
160 crate::body::analyze_end_body(body_node, content, inner_indent)
161 }
162
163 fn extract_module_doc(&self, src: &str) -> Option<String> {
164 extract_ruby_module_doc(src)
165 }
166}
167
168impl LanguageSymbols for Ruby {}
169
170fn extract_ruby_module_doc(src: &str) -> Option<String> {
175 let mut lines = Vec::new();
176 let mut past_magic = false;
177 for line in src.lines() {
178 let trimmed = line.trim();
179 if trimmed.is_empty() {
180 if lines.is_empty() {
181 continue; } else {
183 break; }
185 }
186 if trimmed.starts_with('#') {
187 let text = trimmed.strip_prefix('#').unwrap_or("").trim_start();
188 if !past_magic
190 && (text.starts_with("frozen_string_literal")
191 || text.starts_with("encoding")
192 || text.starts_with("coding"))
193 {
194 continue;
195 }
196 past_magic = true;
197 lines.push(text.to_string());
198 } else {
199 break; }
201 }
202 if lines.is_empty() {
203 return None;
204 }
205 while lines.last().map(|l: &String| l.is_empty()).unwrap_or(false) {
207 lines.pop();
208 }
209 if lines.is_empty() {
210 None
211 } else {
212 Some(lines.join("\n"))
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::validate_unused_kinds_audit;
220
221 #[test]
222 fn unused_node_kinds_audit() {
223 #[rustfmt::skip]
224 let documented_unused: &[&str] = &[
225 "begin_block", "block_argument", "block_body", "block_parameter", "block_parameters",
227 "body_statement", "class_variable", "destructured_left_assignment",
228 "destructured_parameter", "else", "elsif", "empty_statement", "end_block",
229 "exception_variable", "exceptions", "expression_reference_pattern", "forward_argument",
230 "forward_parameter", "heredoc_body", "lambda_parameters",
231 "method_parameters", "operator", "operator_assignment", "parenthesized_statements", "superclass",
232 "case_match", "if_guard", "if_modifier", "in_clause", "match_pattern",
234 "rescue_modifier", "unless_modifier", "until_modifier", "while_modifier",
235 "yield",
237 "case",
239 "while",
240 "block",
241 "retry",
242 "do_block",
243 "return",
244 "for",
245 "if",
246 "lambda",
247 ];
248
249 validate_unused_kinds_audit(&Ruby, documented_unused)
250 .expect("Ruby unused node kinds audit failed");
251 }
252}