Skip to main content

normalize_languages/
vb.rs

1//! Visual Basic language support.
2
3use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use std::path::Path;
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 as_symbols(&self) -> Option<&dyn LanguageSymbols> {
23        Some(self)
24    }
25
26    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
27        if node.kind() != "imports_statement" {
28            return Vec::new();
29        }
30
31        let text = &content[node.byte_range()];
32        vec![Import {
33            module: text.trim().to_string(),
34            names: Vec::new(),
35            alias: None,
36            is_wildcard: false,
37            is_relative: false,
38            line: node.start_position().row + 1,
39        }]
40    }
41
42    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
43        // Visual Basic: Imports Namespace
44        format!("Imports {}", import.module)
45    }
46
47    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
48        let text = &content[node.byte_range()];
49        let lower = text.to_lowercase();
50        if lower.contains("private") {
51            Visibility::Private
52        } else if lower.contains("protected") {
53            Visibility::Protected
54        } else {
55            Visibility::Public
56        }
57    }
58
59    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
60        let name = symbol.name.as_str();
61        match symbol.kind {
62            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
63            crate::SymbolKind::Module => name == "tests" || name == "test",
64            _ => false,
65        }
66    }
67
68    fn test_file_globs(&self) -> &'static [&'static str] {
69        &["**/*Test.vb", "**/*Tests.vb"]
70    }
71
72    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
73        node.child_by_field_name("body")
74    }
75
76    fn analyze_container_body(
77        &self,
78        body_node: &Node,
79        content: &str,
80        inner_indent: &str,
81    ) -> Option<ContainerBody> {
82        crate::body::analyze_end_body(body_node, content, inner_indent)
83    }
84
85    fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
86        static RESOLVER: VBModuleResolver = VBModuleResolver;
87        Some(&RESOLVER)
88    }
89}
90
91impl LanguageSymbols for VB {}
92
93// =============================================================================
94// VB Module Resolver
95// =============================================================================
96
97/// Module resolver for Visual Basic .NET.
98pub struct VBModuleResolver;
99
100impl ModuleResolver for VBModuleResolver {
101    fn workspace_config(&self, root: &Path) -> ResolverConfig {
102        ResolverConfig {
103            workspace_root: root.to_path_buf(),
104            path_mappings: Vec::new(),
105            search_roots: vec![root.to_path_buf()],
106        }
107    }
108
109    fn module_of_file(&self, root: &Path, file: &Path, _cfg: &ResolverConfig) -> Vec<ModuleId> {
110        let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
111        if ext != "vb" && ext != "vbs" {
112            return Vec::new();
113        }
114        if let Ok(rel) = file.strip_prefix(root) {
115            let rel_str = rel
116                .to_str()
117                .unwrap_or("")
118                .trim_end_matches(".vbs")
119                .trim_end_matches(".vb")
120                .replace(['/', '\\'], ".");
121            if !rel_str.is_empty() {
122                return vec![ModuleId {
123                    canonical_path: rel_str,
124                }];
125            }
126        }
127        Vec::new()
128    }
129
130    fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
131        let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
132        if ext != "vb" && ext != "vbs" {
133            return Resolution::NotApplicable;
134        }
135        let raw = &spec.raw;
136        // Strip "Imports " prefix if present (VB stores full "Imports X.Y" as raw)
137        let name = raw.strip_prefix("Imports ").unwrap_or(raw).trim();
138        let exported_name = name.rsplit('.').next().unwrap_or(name).to_string();
139
140        let parts: Vec<&str> = name.split('.').collect();
141        for skip in 0..parts.len() {
142            let path_part = parts[skip..].join("/");
143            let candidate = cfg.workspace_root.join(format!("{}.vb", path_part));
144            if candidate.exists() {
145                return Resolution::Resolved(candidate, exported_name.clone());
146            }
147        }
148        Resolution::NotFound
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::validate_unused_kinds_audit;
156
157    #[test]
158    fn unused_node_kinds_audit() {
159        #[rustfmt::skip]
160        let documented_unused: &[&str] = &[
161            // Block types
162            "namespace_block",
163            // Declaration types
164            "field_declaration", "constructor_declaration", "event_declaration",
165            "type_declaration", "const_declaration", "enum_member",
166            // Statement types
167            "statement", "assignment_statement", "compound_assignment_statement",
168            "call_statement", "dim_statement", "redim_statement", "re_dim_clause",
169            "exit_statement", "continue_statement", "return_statement", "goto_statement",
170            "label_statement", "throw_statement", "empty_statement",
171            // Control flow
172            "try_statement", "catch_block", "finally_block",
173            "case_block", "case_else_block", "else_clause", "elseif_clause",
174            "with_statement", "with_initializer",
175            "using_statement", "sync_lock_statement",
176            // Expression types
177            "expression", "binary_expression", "unary_expression", "ternary_expression",
178            "parenthesized_expression", "lambda_expression", "new_expression",
179            // Type-related
180            "type", "generic_type", "array_type", "primitive_type",
181            "type_parameters", "type_parameter", "type_constraint",
182            "type_argument_list", "array_rank_specifier",
183            // Clauses
184            "as_clause", "inherits_clause", "implements_clause",
185            // Modifiers
186            "modifier", "modifiers",
187            // Event handlers
188            "add_handler_block", "remove_handler_block", "raise_event_block",
189            // Other
190            "identifier", "attribute_block", "option_statements",
191            "relational_operator", "lambda_parameter",
192            // control flow — not extracted as symbols
193            "case_clause",
194            "while_statement",
195            "for_statement",
196            "for_each_statement",
197            "imports_statement",
198            "do_statement",
199            "if_statement",
200            "select_case_statement",
201        ];
202        validate_unused_kinds_audit(&VB, documented_unused)
203            .expect("Visual Basic unused node kinds audit failed");
204    }
205}