Skip to main content

normalize_languages/
matlab.rs

1//! MATLAB language support.
2
3use std::path::{Path, PathBuf};
4
5use crate::{
6    ContainerBody, Import, ImportSpec, Language, LanguageSymbols, ModuleId, ModuleResolver,
7    Resolution, ResolverConfig,
8};
9use tree_sitter::Node;
10
11/// MATLAB language support.
12pub 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        // MATLAB: import package.* or import package.function
51        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        // MATLAB class_definition has no dedicated body field; use node itself
69        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        // classdef Foo\n  methods...\nend — skip first line, strip `end`
79        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
103// =============================================================================
104// MATLAB Module Resolver
105// =============================================================================
106
107/// Module resolver for MATLAB.
108///
109/// In MATLAB, one function or class lives in each `.m` file, and the filename
110/// is the function/class name. Functions are found on the path — there is no
111/// explicit import syntax (the `import` command is for Java class imports, not
112/// MATLAB function lookup). `ImportSpec.raw` will be a function/class name.
113pub 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        // Also search common subdirectories
119        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        // In MATLAB, the function/class name = filename stem
139        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        // Look for <name>.m in search_roots and from_file's directory
156        let filename = format!("{}.m", raw);
157
158        // Check caller's directory first
159        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        // Search configured roots
167        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            // Operators
188            "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            // Statements
192            "arguments_statement", "break_statement", "continue_statement", "return_statement",
193            "spmd_statement",
194            // Control flow clauses
195            "case_clause", "else_clause", "elseif_clause", "otherwise_clause",
196            // Class-related
197            "class_property", "enum", "enumeration", "superclass", "superclasses",
198            // Function-related
199            "block", "field_expression", "formatting_sequence", "function_arguments",
200            "function_call", "function_output", "function_signature", "identifier", "lambda",
201            "parfor_options", "validation_functions",
202            // control flow — not extracted as symbols
203            "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}