1use std::path::{Path, PathBuf};
4
5use crate::{
6 ContainerBody, Import, ImportSpec, Language, LanguageSymbols, ModuleId, ModuleResolver,
7 Resolution, ResolverConfig,
8};
9use tree_sitter::Node;
10
11pub struct Matlab;
13
14impl Language for Matlab {
15 fn name(&self) -> &'static str {
16 "MATLAB"
17 }
18 fn extensions(&self) -> &'static [&'static str] {
19 &["m"]
20 }
21 fn grammar_name(&self) -> &'static str {
22 "matlab"
23 }
24
25 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
26 Some(self)
27 }
28
29 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
30 if node.kind() != "command" {
31 return Vec::new();
32 }
33
34 let text = &content[node.byte_range()];
35 if !text.starts_with("import ") {
36 return Vec::new();
37 }
38
39 vec![Import {
40 module: text[7..].trim().to_string(),
41 names: Vec::new(),
42 alias: None,
43 is_wildcard: text.contains('*'),
44 is_relative: false,
45 line: node.start_position().row + 1,
46 }]
47 }
48
49 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
50 if import.is_wildcard {
52 format!("import {}.*", import.module)
53 } else {
54 format!("import {}", import.module)
55 }
56 }
57
58 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
59 let name = symbol.name.as_str();
60 match symbol.kind {
61 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
62 crate::SymbolKind::Module => name == "tests" || name == "test",
63 _ => false,
64 }
65 }
66
67 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
68 Some(*node)
70 }
71
72 fn analyze_container_body(
73 &self,
74 body_node: &Node,
75 content: &str,
76 inner_indent: &str,
77 ) -> Option<ContainerBody> {
78 crate::body::analyze_keyword_end_body(body_node, content, inner_indent)
80 }
81
82 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
83 if let Some(name_node) = node.child_by_field_name("name") {
84 return Some(&content[name_node.byte_range()]);
85 }
86 let mut cursor = node.walk();
87 for child in node.children(&mut cursor) {
88 if child.kind() == "identifier" {
89 return Some(&content[child.byte_range()]);
90 }
91 }
92 None
93 }
94
95 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
96 static RESOLVER: MatlabModuleResolver = MatlabModuleResolver;
97 Some(&RESOLVER)
98 }
99}
100
101impl LanguageSymbols for Matlab {}
102
103pub struct MatlabModuleResolver;
114
115impl ModuleResolver for MatlabModuleResolver {
116 fn workspace_config(&self, root: &Path) -> ResolverConfig {
117 let mut search_roots: Vec<PathBuf> = vec![root.to_path_buf()];
118 for subdir in &["src", "lib"] {
120 let candidate = root.join(subdir);
121 if candidate.is_dir() {
122 search_roots.push(candidate);
123 }
124 }
125 ResolverConfig {
126 workspace_root: root.to_path_buf(),
127 path_mappings: Vec::new(),
128 search_roots,
129 }
130 }
131
132 fn module_of_file(&self, _root: &Path, file: &Path, _cfg: &ResolverConfig) -> Vec<ModuleId> {
133 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
134 if ext != "m" {
135 return Vec::new();
136 }
137
138 if let Some(stem) = file.file_stem().and_then(|s| s.to_str()) {
140 return vec![ModuleId {
141 canonical_path: stem.to_string(),
142 }];
143 }
144
145 Vec::new()
146 }
147
148 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
149 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
150 if ext != "m" {
151 return Resolution::NotApplicable;
152 }
153
154 let raw = &spec.raw;
155 let filename = format!("{}.m", raw);
157
158 if let Some(dir) = from_file.parent() {
160 let candidate = dir.join(&filename);
161 if candidate.exists() {
162 return Resolution::Resolved(candidate, String::new());
163 }
164 }
165
166 for root in &cfg.search_roots {
168 let candidate = root.join(&filename);
169 if candidate.exists() {
170 return Resolution::Resolved(candidate, String::new());
171 }
172 }
173
174 Resolution::NotFound
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::validate_unused_kinds_audit;
182
183 #[test]
184 fn unused_node_kinds_audit() {
185 #[rustfmt::skip]
186 let documented_unused: &[&str] = &[
187 "binary_operator", "boolean_operator", "comparison_operator", "global_operator",
189 "handle_operator", "metaclass_operator", "not_operator", "persistent_operator",
190 "postfix_operator", "spread_operator", "unary_operator",
191 "arguments_statement", "break_statement", "continue_statement", "return_statement",
193 "spmd_statement",
194 "case_clause", "else_clause", "elseif_clause", "otherwise_clause",
196 "class_property", "enum", "enumeration", "superclass", "superclasses",
198 "block", "field_expression", "formatting_sequence", "function_arguments",
200 "function_call", "function_output", "function_signature", "identifier", "lambda",
201 "parfor_options", "validation_functions",
202 "if_statement",
204 "catch_clause",
205 "switch_statement",
206 "while_statement",
207 "for_statement",
208 "try_statement",
209 "methods",
210 ];
211 validate_unused_kinds_audit(&Matlab, documented_unused)
212 .expect("MATLAB unused node kinds audit failed");
213 }
214}