Skip to main content

normalize_languages/
vb.rs

1//! Visual Basic language 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/// Visual Basic language support.
9pub struct VB;
10
11impl Language for VB {
12    fn name(&self) -> &'static str {
13        "Visual Basic"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["vb", "vbs"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "vb"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &[
28            "class_block",
29            "module_block",
30            "structure_block",
31            "interface_block",
32        ]
33    }
34
35    fn function_kinds(&self) -> &'static [&'static str] {
36        &["method_declaration", "property_declaration"]
37    }
38
39    fn type_kinds(&self) -> &'static [&'static str] {
40        &["enum_block", "delegate_declaration"]
41    }
42
43    fn import_kinds(&self) -> &'static [&'static str] {
44        &["imports_statement"]
45    }
46
47    fn public_symbol_kinds(&self) -> &'static [&'static str] {
48        &["class_block", "module_block", "method_declaration"]
49    }
50
51    fn visibility_mechanism(&self) -> VisibilityMechanism {
52        VisibilityMechanism::AccessModifier
53    }
54
55    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
56        match node.kind() {
57            "class_block" | "module_block" | "structure_block" => {
58                if let Some(name) = self.node_name(node, content) {
59                    return vec![Export {
60                        name: name.to_string(),
61                        kind: SymbolKind::Class,
62                        line: node.start_position().row + 1,
63                    }];
64                }
65            }
66            "method_declaration" => {
67                if let Some(name) = self.node_name(node, content) {
68                    return vec![Export {
69                        name: name.to_string(),
70                        kind: SymbolKind::Function,
71                        line: node.start_position().row + 1,
72                    }];
73                }
74            }
75            _ => {}
76        }
77        Vec::new()
78    }
79
80    fn scope_creating_kinds(&self) -> &'static [&'static str] {
81        &["class_block", "module_block", "method_declaration"]
82    }
83
84    fn control_flow_kinds(&self) -> &'static [&'static str] {
85        &[
86            "if_statement",
87            "select_case_statement",
88            "while_statement",
89            "for_statement",
90            "for_each_statement",
91            "do_statement",
92        ]
93    }
94
95    fn complexity_nodes(&self) -> &'static [&'static str] {
96        &[
97            "if_statement",
98            "select_case_statement",
99            "while_statement",
100            "for_statement",
101            "for_each_statement",
102            "case_clause",
103        ]
104    }
105
106    fn nesting_nodes(&self) -> &'static [&'static str] {
107        &[
108            "if_statement",
109            "select_case_statement",
110            "while_statement",
111            "for_statement",
112            "do_statement",
113        ]
114    }
115
116    fn signature_suffix(&self) -> &'static str {
117        ""
118    }
119
120    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
121        match node.kind() {
122            "method_declaration" | "property_declaration" => {
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: self.get_visibility(node, content),
136                    children: Vec::new(),
137                    is_interface_impl: false,
138                    implements: Vec::new(),
139                })
140            }
141            _ => None,
142        }
143    }
144
145    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
146        match node.kind() {
147            "class_block" | "module_block" | "structure_block" | "interface_block" => {
148                let name = self.node_name(node, content)?;
149                let text = &content[node.byte_range()];
150                let first_line = text.lines().next().unwrap_or(text);
151
152                Some(Symbol {
153                    name: name.to_string(),
154                    kind: SymbolKind::Class,
155                    signature: first_line.trim().to_string(),
156                    docstring: None,
157                    attributes: Vec::new(),
158                    start_line: node.start_position().row + 1,
159                    end_line: node.end_position().row + 1,
160                    visibility: self.get_visibility(node, content),
161                    children: Vec::new(),
162                    is_interface_impl: false,
163                    implements: Vec::new(),
164                })
165            }
166            _ => None,
167        }
168    }
169
170    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
171        match node.kind() {
172            "enum_block" | "delegate_declaration" => {
173                let name = self.node_name(node, content)?;
174                let text = &content[node.byte_range()];
175                let first_line = text.lines().next().unwrap_or(text);
176
177                Some(Symbol {
178                    name: name.to_string(),
179                    kind: SymbolKind::Type,
180                    signature: first_line.trim().to_string(),
181                    docstring: None,
182                    attributes: Vec::new(),
183                    start_line: node.start_position().row + 1,
184                    end_line: node.end_position().row + 1,
185                    visibility: self.get_visibility(node, content),
186                    children: Vec::new(),
187                    is_interface_impl: false,
188                    implements: Vec::new(),
189                })
190            }
191            _ => None,
192        }
193    }
194
195    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
196        None
197    }
198
199    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
200        Vec::new()
201    }
202
203    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
204        if node.kind() != "imports_statement" {
205            return Vec::new();
206        }
207
208        let text = &content[node.byte_range()];
209        vec![Import {
210            module: text.trim().to_string(),
211            names: Vec::new(),
212            alias: None,
213            is_wildcard: false,
214            is_relative: false,
215            line: node.start_position().row + 1,
216        }]
217    }
218
219    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
220        // Visual Basic: Imports Namespace
221        format!("Imports {}", import.module)
222    }
223
224    fn is_public(&self, node: &Node, content: &str) -> bool {
225        let text = &content[node.byte_range()];
226        text.to_lowercase().contains("public") || !text.to_lowercase().contains("private")
227    }
228
229    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
230        let text = &content[node.byte_range()];
231        let lower = text.to_lowercase();
232        if lower.contains("private") {
233            Visibility::Private
234        } else if lower.contains("protected") {
235            Visibility::Protected
236        } else {
237            Visibility::Public
238        }
239    }
240
241    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
242        let name = symbol.name.as_str();
243        match symbol.kind {
244            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
245            crate::SymbolKind::Module => name == "tests" || name == "test",
246            _ => false,
247        }
248    }
249
250    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
251        None
252    }
253
254    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
255        node.child_by_field_name("body")
256    }
257
258    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
259        false
260    }
261
262    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
263        node.child_by_field_name("name")
264            .map(|n| &content[n.byte_range()])
265    }
266
267    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
268        let ext = path.extension()?.to_str()?;
269        if !["vb", "vbs"].contains(&ext) {
270            return None;
271        }
272        let stem = path.file_stem()?.to_str()?;
273        Some(stem.to_string())
274    }
275
276    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
277        vec![format!("{}.vb", module), format!("{}.vbs", module)]
278    }
279
280    fn lang_key(&self) -> &'static str {
281        "vb"
282    }
283
284    fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
285        import_name.starts_with("System.") || import_name.starts_with("Microsoft.")
286    }
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        &["vb", "vbs"]
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(".vb")
325            .or_else(|| entry_name.strip_suffix(".vbs"))
326            .unwrap_or(entry_name)
327            .to_string()
328    }
329
330    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
331        if path.is_file() {
332            Some(path.to_path_buf())
333        } else {
334            None
335        }
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::validate_unused_kinds_audit;
343
344    #[test]
345    fn unused_node_kinds_audit() {
346        #[rustfmt::skip]
347        let documented_unused: &[&str] = &[
348            // Block types
349            "namespace_block",
350            // Declaration types
351            "field_declaration", "constructor_declaration", "event_declaration",
352            "type_declaration", "const_declaration", "enum_member",
353            // Statement types
354            "statement", "assignment_statement", "compound_assignment_statement",
355            "call_statement", "dim_statement", "redim_statement", "re_dim_clause",
356            "exit_statement", "continue_statement", "return_statement", "goto_statement",
357            "label_statement", "throw_statement", "empty_statement",
358            // Control flow
359            "try_statement", "catch_block", "finally_block",
360            "case_block", "case_else_block", "else_clause", "elseif_clause",
361            "with_statement", "with_initializer",
362            "using_statement", "sync_lock_statement",
363            // Expression types
364            "expression", "binary_expression", "unary_expression", "ternary_expression",
365            "parenthesized_expression", "lambda_expression", "new_expression",
366            // Type-related
367            "type", "generic_type", "array_type", "primitive_type",
368            "type_parameters", "type_parameter", "type_constraint",
369            "type_argument_list", "array_rank_specifier",
370            // Clauses
371            "as_clause", "inherits_clause", "implements_clause",
372            // Modifiers
373            "modifier", "modifiers",
374            // Event handlers
375            "add_handler_block", "remove_handler_block", "raise_event_block",
376            // Other
377            "identifier", "attribute_block", "option_statements",
378            "relational_operator", "lambda_parameter",
379        ];
380        validate_unused_kinds_audit(&VB, documented_unused)
381            .expect("Visual Basic unused node kinds audit failed");
382    }
383}