Skip to main content

normalize_languages/
batch.rs

1//! Windows Batch file support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// Batch language support.
9pub struct Batch;
10
11impl Language for Batch {
12    fn name(&self) -> &'static str {
13        "Batch"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["bat", "cmd"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "batch"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &[]
28    }
29
30    fn function_kinds(&self) -> &'static [&'static str] {
31        &["function_definition"]
32    }
33
34    fn type_kinds(&self) -> &'static [&'static str] {
35        &[]
36    }
37
38    fn import_kinds(&self) -> &'static [&'static str] {
39        &[] // batch grammar doesn't have import nodes
40    }
41
42    fn public_symbol_kinds(&self) -> &'static [&'static str] {
43        &["function_definition", "variable_declaration"]
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            "variable_declaration" => {
62                if let Some(name) = self.node_name(node, content) {
63                    return vec![Export {
64                        name: name.to_string(),
65                        kind: SymbolKind::Variable,
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        &["function_definition"]
77    }
78
79    fn control_flow_kinds(&self) -> &'static [&'static str] {
80        &[] // batch grammar doesn't have control flow nodes
81    }
82
83    fn complexity_nodes(&self) -> &'static [&'static str] {
84        &[]
85    }
86
87    fn nesting_nodes(&self) -> &'static [&'static str] {
88        &[]
89    }
90
91    fn signature_suffix(&self) -> &'static str {
92        ""
93    }
94
95    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
96        if node.kind() != "function_definition" {
97            return None;
98        }
99
100        let name = self.node_name(node, content)?;
101        let text = &content[node.byte_range()];
102
103        Some(Symbol {
104            name: name.to_string(),
105            kind: SymbolKind::Function,
106            signature: text.trim().to_string(),
107            docstring: None,
108            attributes: Vec::new(),
109            start_line: node.start_position().row + 1,
110            end_line: node.end_position().row + 1,
111            visibility: Visibility::Public,
112            children: Vec::new(),
113            is_interface_impl: false,
114            implements: Vec::new(),
115        })
116    }
117
118    fn extract_container(&self, _node: &Node, _content: &str) -> Option<Symbol> {
119        None
120    }
121    fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
122        None
123    }
124    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
125        None
126    }
127
128    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
129        Vec::new()
130    }
131
132    fn extract_imports(&self, _node: &Node, _content: &str) -> Vec<Import> {
133        Vec::new() // batch grammar doesn't have import nodes
134    }
135
136    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
137        // Batch: call script.bat
138        format!("call {}", import.module)
139    }
140
141    fn is_public(&self, _node: &Node, _content: &str) -> bool {
142        true
143    }
144    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
145        Visibility::Public
146    }
147
148    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
149        let name = symbol.name.as_str();
150        match symbol.kind {
151            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
152            crate::SymbolKind::Module => name == "tests" || name == "test",
153            _ => false,
154        }
155    }
156
157    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
158        None
159    }
160
161    fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
162        None
163    }
164    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
165        false
166    }
167
168    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
169        if let Some(name_node) = node.child_by_field_name("name") {
170            return Some(&content[name_node.byte_range()]);
171        }
172        let mut cursor = node.walk();
173        for child in node.children(&mut cursor) {
174            if child.kind() == "identifier" {
175                return Some(&content[child.byte_range()]);
176            }
177        }
178        None
179    }
180
181    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
182        let ext = path.extension()?.to_str()?;
183        if !["bat", "cmd"].contains(&ext) {
184            return None;
185        }
186        let stem = path.file_stem()?.to_str()?;
187        Some(stem.to_string())
188    }
189
190    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
191        vec![format!("{}.bat", module), format!("{}.cmd", module)]
192    }
193
194    fn lang_key(&self) -> &'static str {
195        "batch"
196    }
197
198    fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
199        false
200    }
201    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
202        None
203    }
204    fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
205        None
206    }
207    fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
208        None
209    }
210    fn get_version(&self, _: &Path) -> Option<String> {
211        None
212    }
213    fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
214        None
215    }
216    fn indexable_extensions(&self) -> &'static [&'static str] {
217        &["bat", "cmd"]
218    }
219    fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
220        Vec::new()
221    }
222
223    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
224        use crate::traits::{has_extension, skip_dotfiles};
225        if skip_dotfiles(name) {
226            return true;
227        }
228        !is_dir && !has_extension(name, self.indexable_extensions())
229    }
230
231    fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
232        Vec::new()
233    }
234
235    fn package_module_name(&self, entry_name: &str) -> String {
236        entry_name
237            .strip_suffix(".bat")
238            .or_else(|| entry_name.strip_suffix(".cmd"))
239            .unwrap_or(entry_name)
240            .to_string()
241    }
242
243    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
244        if path.is_file() {
245            Some(path.to_path_buf())
246        } else {
247            None
248        }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::validate_unused_kinds_audit;
256
257    #[test]
258    fn unused_node_kinds_audit() {
259        #[rustfmt::skip]
260        let documented_unused: &[&str] = &[
261            "identifier",
262        ];
263        validate_unused_kinds_audit(&Batch, documented_unused)
264            .expect("Batch unused node kinds audit failed");
265    }
266}