1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub struct Ruby;
10
11impl Language for Ruby {
12 fn name(&self) -> &'static str {
13 "Ruby"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["rb"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "ruby"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &["class", "module"]
28 }
29 fn function_kinds(&self) -> &'static [&'static str] {
30 &["method", "singleton_method"]
31 }
32 fn type_kinds(&self) -> &'static [&'static str] {
33 &["class", "module"]
34 }
35 fn import_kinds(&self) -> &'static [&'static str] {
36 &["call"] }
38
39 fn public_symbol_kinds(&self) -> &'static [&'static str] {
40 &["class", "module", "method", "singleton_method"]
41 }
42
43 fn visibility_mechanism(&self) -> VisibilityMechanism {
44 VisibilityMechanism::AllPublic }
46
47 fn scope_creating_kinds(&self) -> &'static [&'static str] {
48 &["do_block", "block", "lambda", "for"]
49 }
50
51 fn control_flow_kinds(&self) -> &'static [&'static str] {
52 &[
53 "if", "unless", "case", "while", "until", "for", "return", "break", "next", "redo",
54 "retry", "begin",
55 ]
56 }
57
58 fn complexity_nodes(&self) -> &'static [&'static str] {
59 &[
60 "if",
61 "unless",
62 "case",
63 "when",
64 "while",
65 "until",
66 "for",
67 "begin", "rescue",
69 "and",
70 "or",
71 "conditional",
72 ]
73 }
74
75 fn nesting_nodes(&self) -> &'static [&'static str] {
76 &[
77 "if",
78 "unless",
79 "case",
80 "while",
81 "until",
82 "for",
83 "begin",
84 "method",
85 "singleton_method",
86 "class",
87 "module",
88 "do_block",
89 "block",
90 ]
91 }
92
93 fn signature_suffix(&self) -> &'static str {
94 "; end"
95 }
96
97 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
98 let name = self.node_name(node, content)?;
99 Some(Symbol {
100 name: name.to_string(),
101 kind: SymbolKind::Method,
102 signature: format!("def {}", name),
103 docstring: None,
104 attributes: Vec::new(),
105 start_line: node.start_position().row + 1,
106 end_line: node.end_position().row + 1,
107 visibility: Visibility::Public,
108 children: Vec::new(),
109 is_interface_impl: false,
110 implements: Vec::new(),
111 })
112 }
113
114 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
115 let name = self.node_name(node, content)?;
116 let kind = if node.kind() == "module" {
117 SymbolKind::Module
118 } else {
119 SymbolKind::Class
120 };
121
122 Some(Symbol {
123 name: name.to_string(),
124 kind,
125 signature: format!("{} {}", kind.as_str(), name),
126 docstring: None,
127 attributes: Vec::new(),
128 start_line: node.start_position().row + 1,
129 end_line: node.end_position().row + 1,
130 visibility: Visibility::Public,
131 children: Vec::new(),
132 is_interface_impl: false,
133 implements: Vec::new(),
134 })
135 }
136
137 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
138 self.extract_container(node, content)
139 }
140
141 fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
142 None
143 }
144
145 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
146 Vec::new()
147 }
148 fn extract_imports(&self, _node: &Node, _content: &str) -> Vec<Import> {
149 Vec::new()
150 }
151
152 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
153 if import.is_relative {
155 format!("require_relative '{}'", import.module)
156 } else {
157 format!("require '{}'", import.module)
158 }
159 }
160
161 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
162 let name = match self.node_name(node, content) {
163 Some(n) => n.to_string(),
164 None => return Vec::new(),
165 };
166 let kind = match node.kind() {
167 "class" => SymbolKind::Class,
168 "module" => SymbolKind::Module,
169 "method" | "singleton_method" => SymbolKind::Method,
170 _ => return Vec::new(),
171 };
172 vec![Export {
173 name,
174 kind,
175 line: node.start_position().row + 1,
176 }]
177 }
178
179 fn is_public(&self, _node: &Node, _content: &str) -> bool {
180 true
181 }
182 fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
183 Visibility::Public
184 }
185
186 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
187 let name = symbol.name.as_str();
188 match symbol.kind {
189 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
190 crate::SymbolKind::Module => name == "tests" || name == "test",
191 _ => false,
192 }
193 }
194
195 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
196 None
197 }
198
199 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
200 node.child_by_field_name("body")
201 }
202 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
203 false
204 }
205
206 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
207 let name_node = node.child_by_field_name("name")?;
208 Some(&content[name_node.byte_range()])
209 }
210
211 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
212 if path.extension()?.to_str()? != "rb" {
213 return None;
214 }
215 Some(path.to_string_lossy().to_string())
216 }
217 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
218 vec![format!("{}.rb", module)]
219 }
220
221 fn lang_key(&self) -> &'static str {
222 "ruby"
223 }
224 fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
225 None
226 }
227 fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
228 None
229 }
230 fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
231 false
232 }
233 fn get_version(&self, _: &Path) -> Option<String> {
234 None
235 }
236 fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
237 None
238 }
239 fn indexable_extensions(&self) -> &'static [&'static str] {
240 &["rb"]
241 }
242 fn find_stdlib(&self, _: &Path) -> Option<PathBuf> {
243 None
244 }
245 fn package_module_name(&self, name: &str) -> String {
246 name.strip_suffix(".rb").unwrap_or(name).to_string()
247 }
248 fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
249 Vec::new()
250 }
251 fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
252 Vec::new()
253 }
254 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
255 if path.is_file() {
256 Some(path.to_path_buf())
257 } else {
258 None
259 }
260 }
261
262 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
263 use crate::traits::{has_extension, skip_dotfiles};
264 if skip_dotfiles(name) {
265 return true;
266 }
267 !is_dir && !has_extension(name, self.indexable_extensions())
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::validate_unused_kinds_audit;
275
276 #[test]
277 fn unused_node_kinds_audit() {
278 #[rustfmt::skip]
279 let documented_unused: &[&str] = &[
280 "begin_block", "block_argument", "block_body", "block_parameter", "block_parameters",
282 "body_statement", "class_variable", "destructured_left_assignment",
283 "destructured_parameter", "else", "elsif", "empty_statement", "end_block",
284 "exception_variable", "exceptions", "expression_reference_pattern", "forward_argument",
285 "forward_parameter", "heredoc_body", "identifier", "lambda_parameters",
286 "method_parameters", "operator", "operator_assignment", "parenthesized_statements",
287 "singleton_class", "superclass",
288 "case_match", "if_guard", "if_modifier", "in_clause", "match_pattern",
290 "rescue_modifier", "unless_modifier", "until_modifier", "while_modifier",
291 "yield",
293 ];
294
295 validate_unused_kinds_audit(&Ruby, documented_unused)
296 .expect("Ruby unused node kinds audit failed");
297 }
298}