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 Matlab;
10
11impl Language for Matlab {
12 fn name(&self) -> &'static str {
13 "MATLAB"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["m"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "matlab"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &["class_definition"]
28 }
29
30 fn function_kinds(&self) -> &'static [&'static str] {
31 &["function_definition"]
32 }
33
34 fn type_kinds(&self) -> &'static [&'static str] {
35 &["class_definition"]
36 }
37
38 fn import_kinds(&self) -> &'static [&'static str] {
39 &["command"] }
41
42 fn public_symbol_kinds(&self) -> &'static [&'static str] {
43 &["function_definition", "class_definition"]
44 }
45
46 fn visibility_mechanism(&self) -> VisibilityMechanism {
47 VisibilityMechanism::AllPublic
48 }
49
50 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51 match node.kind() {
52 "function_definition" => {
53 if let Some(name) = self.node_name(node, content) {
54 return vec![Export {
55 name: name.to_string(),
56 kind: SymbolKind::Function,
57 line: node.start_position().row + 1,
58 }];
59 }
60 }
61 "class_definition" => {
62 if let Some(name) = self.node_name(node, content) {
63 return vec![Export {
64 name: name.to_string(),
65 kind: SymbolKind::Class,
66 line: node.start_position().row + 1,
67 }];
68 }
69 }
70 _ => {}
71 }
72 Vec::new()
73 }
74
75 fn scope_creating_kinds(&self) -> &'static [&'static str] {
76 &[
77 "function_definition",
78 "class_definition",
79 "methods",
80 "properties",
81 ]
82 }
83
84 fn control_flow_kinds(&self) -> &'static [&'static str] {
85 &[
86 "if_statement",
87 "switch_statement",
88 "while_statement",
89 "for_statement",
90 "try_statement",
91 ]
92 }
93
94 fn complexity_nodes(&self) -> &'static [&'static str] {
95 &[
96 "if_statement",
97 "switch_statement",
98 "while_statement",
99 "for_statement",
100 "catch_clause",
101 ]
102 }
103
104 fn nesting_nodes(&self) -> &'static [&'static str] {
105 &[
106 "if_statement",
107 "switch_statement",
108 "while_statement",
109 "for_statement",
110 "function_definition",
111 ]
112 }
113
114 fn signature_suffix(&self) -> &'static str {
115 ""
116 }
117
118 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
119 if node.kind() != "function_definition" {
120 return None;
121 }
122
123 let name = self.node_name(node, content)?;
124 let text = &content[node.byte_range()];
125 let first_line = text.lines().next().unwrap_or(text);
126
127 Some(Symbol {
128 name: name.to_string(),
129 kind: SymbolKind::Function,
130 signature: first_line.trim().to_string(),
131 docstring: None,
132 attributes: Vec::new(),
133 start_line: node.start_position().row + 1,
134 end_line: node.end_position().row + 1,
135 visibility: Visibility::Public,
136 children: Vec::new(),
137 is_interface_impl: false,
138 implements: Vec::new(),
139 })
140 }
141
142 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
143 if node.kind() != "class_definition" {
144 return None;
145 }
146
147 let name = self.node_name(node, content)?;
148 let text = &content[node.byte_range()];
149 let first_line = text.lines().next().unwrap_or(text);
150
151 Some(Symbol {
152 name: name.to_string(),
153 kind: SymbolKind::Class,
154 signature: first_line.trim().to_string(),
155 docstring: None,
156 attributes: Vec::new(),
157 start_line: node.start_position().row + 1,
158 end_line: node.end_position().row + 1,
159 visibility: Visibility::Public,
160 children: Vec::new(),
161 is_interface_impl: false,
162 implements: Vec::new(),
163 })
164 }
165
166 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
167 if node.kind() != "class_definition" {
168 return None;
169 }
170
171 let name = self.node_name(node, content)?;
172 let text = &content[node.byte_range()];
173 let first_line = text.lines().next().unwrap_or(text);
174
175 Some(Symbol {
176 name: name.to_string(),
177 kind: SymbolKind::Type,
178 signature: first_line.trim().to_string(),
179 docstring: None,
180 attributes: Vec::new(),
181 start_line: node.start_position().row + 1,
182 end_line: node.end_position().row + 1,
183 visibility: Visibility::Public,
184 children: Vec::new(),
185 is_interface_impl: false,
186 implements: Vec::new(),
187 })
188 }
189
190 fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
191 None
192 }
193
194 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
195 Vec::new()
196 }
197
198 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
199 if node.kind() != "command" {
200 return Vec::new();
201 }
202
203 let text = &content[node.byte_range()];
204 if !text.starts_with("import ") {
205 return Vec::new();
206 }
207
208 vec![Import {
209 module: text[7..].trim().to_string(),
210 names: Vec::new(),
211 alias: None,
212 is_wildcard: text.contains('*'),
213 is_relative: false,
214 line: node.start_position().row + 1,
215 }]
216 }
217
218 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
219 if import.is_wildcard {
221 format!("import {}.*", import.module)
222 } else {
223 format!("import {}", import.module)
224 }
225 }
226
227 fn is_public(&self, _node: &Node, _content: &str) -> bool {
228 true
229 }
230 fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
231 Visibility::Public
232 }
233
234 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
235 let name = symbol.name.as_str();
236 match symbol.kind {
237 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
238 crate::SymbolKind::Module => name == "tests" || name == "test",
239 _ => false,
240 }
241 }
242
243 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
244 None
245 }
246
247 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
248 node.child_by_field_name("body")
249 }
250
251 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
252 false
253 }
254
255 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
256 if let Some(name_node) = node.child_by_field_name("name") {
257 return Some(&content[name_node.byte_range()]);
258 }
259 let mut cursor = node.walk();
260 for child in node.children(&mut cursor) {
261 if child.kind() == "identifier" {
262 return Some(&content[child.byte_range()]);
263 }
264 }
265 None
266 }
267
268 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
269 let ext = path.extension()?.to_str()?;
270 if ext != "m" {
271 return None;
272 }
273 let stem = path.file_stem()?.to_str()?;
274 Some(stem.to_string())
275 }
276
277 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
278 vec![format!("{}.m", module)]
279 }
280
281 fn lang_key(&self) -> &'static str {
282 "matlab"
283 }
284
285 fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
286 false
287 }
288 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
289 None
290 }
291 fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
292 None
293 }
294 fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
295 None
296 }
297 fn get_version(&self, _: &Path) -> Option<String> {
298 None
299 }
300 fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
301 None
302 }
303 fn indexable_extensions(&self) -> &'static [&'static str] {
304 &["m"]
305 }
306 fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
307 Vec::new()
308 }
309
310 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
311 use crate::traits::{has_extension, skip_dotfiles};
312 if skip_dotfiles(name) {
313 return true;
314 }
315 !is_dir && !has_extension(name, self.indexable_extensions())
316 }
317
318 fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
319 Vec::new()
320 }
321
322 fn package_module_name(&self, entry_name: &str) -> String {
323 entry_name
324 .strip_suffix(".m")
325 .unwrap_or(entry_name)
326 .to_string()
327 }
328
329 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
330 if path.is_file() {
331 Some(path.to_path_buf())
332 } else {
333 None
334 }
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::validate_unused_kinds_audit;
342
343 #[test]
344 fn unused_node_kinds_audit() {
345 #[rustfmt::skip]
346 let documented_unused: &[&str] = &[
347 "binary_operator", "boolean_operator", "comparison_operator", "global_operator",
349 "handle_operator", "metaclass_operator", "not_operator", "persistent_operator",
350 "postfix_operator", "spread_operator", "unary_operator",
351 "arguments_statement", "break_statement", "continue_statement", "return_statement",
353 "spmd_statement",
354 "case_clause", "else_clause", "elseif_clause", "otherwise_clause",
356 "class_property", "enum", "enumeration", "superclass", "superclasses",
358 "block", "field_expression", "formatting_sequence", "function_arguments",
360 "function_call", "function_output", "function_signature", "identifier", "lambda",
361 "parfor_options", "validation_functions",
362 ];
363 validate_unused_kinds_audit(&Matlab, documented_unused)
364 .expect("MATLAB unused node kinds audit failed");
365 }
366}