1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use std::path::Path;
6use tree_sitter::Node;
7
8pub 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 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
93pub 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 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 "namespace_block",
163 "field_declaration", "constructor_declaration", "event_declaration",
165 "type_declaration", "const_declaration", "enum_member",
166 "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 "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", "binary_expression", "unary_expression", "ternary_expression",
178 "parenthesized_expression", "lambda_expression", "new_expression",
179 "type", "generic_type", "array_type", "primitive_type",
181 "type_parameters", "type_parameter", "type_constraint",
182 "type_argument_list", "array_rank_specifier",
183 "as_clause", "inherits_clause", "implements_clause",
185 "modifier", "modifiers",
187 "add_handler_block", "remove_handler_block", "raise_event_block",
189 "identifier", "attribute_block", "option_statements",
191 "relational_operator", "lambda_parameter",
192 "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}