1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub struct Vim;
10
11impl Language for Vim {
12 fn name(&self) -> &'static str {
13 "Vim"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["vim", "vimrc"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "vim"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &["function_definition", "augroup"]
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 &["source_statement", "runtime_statement"]
40 }
41
42 fn public_symbol_kinds(&self) -> &'static [&'static str] {
43 &["function_definition"]
44 }
45
46 fn visibility_mechanism(&self) -> VisibilityMechanism {
47 VisibilityMechanism::NamingConvention }
49
50 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51 let name = match self.node_name(node, content) {
52 Some(n) => n.to_string(),
53 None => return Vec::new(),
54 };
55
56 if name.starts_with("s:") {
58 return Vec::new();
59 }
60
61 vec![Export {
62 name,
63 kind: SymbolKind::Function,
64 line: node.start_position().row + 1,
65 }]
66 }
67
68 fn scope_creating_kinds(&self) -> &'static [&'static str] {
69 &["function_definition"]
70 }
71
72 fn control_flow_kinds(&self) -> &'static [&'static str] {
73 &["if_statement", "for_loop", "while_loop", "try_statement"]
74 }
75
76 fn complexity_nodes(&self) -> &'static [&'static str] {
77 &["if_statement", "elseif_statement", "for_loop", "while_loop"]
78 }
79
80 fn nesting_nodes(&self) -> &'static [&'static str] {
81 &["function_definition", "if_statement", "for_loop"]
82 }
83
84 fn signature_suffix(&self) -> &'static str {
85 ""
86 }
87
88 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
89 let name = self.node_name(node, content)?;
90 let text = &content[node.byte_range()];
91 let first_line = text.lines().next().unwrap_or(text);
92
93 let visibility = if name.starts_with("s:") {
94 Visibility::Private
95 } else {
96 Visibility::Public
97 };
98
99 Some(Symbol {
100 name: name.to_string(),
101 kind: SymbolKind::Function,
102 signature: first_line.trim().to_string(),
103 docstring: self.extract_docstring(node, content),
104 attributes: Vec::new(),
105 start_line: node.start_position().row + 1,
106 end_line: node.end_position().row + 1,
107 visibility,
108 children: Vec::new(),
109 is_interface_impl: false,
110 implements: Vec::new(),
111 })
112 }
113
114 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
115 if node.kind() == "augroup" {
116 let text = &content[node.byte_range()];
117 let name = text
118 .split_whitespace()
119 .nth(1)
120 .unwrap_or("unnamed")
121 .to_string();
122 return Some(Symbol {
123 name: name.clone(),
124 kind: SymbolKind::Module,
125 signature: format!("augroup {}", name),
126 docstring: None,
127 attributes: Vec::new(),
128 start_line: node.start_position().row + 1,
129 end_line: node.end_position().row + 1,
130 visibility: Visibility::Public,
131 children: Vec::new(),
132 is_interface_impl: false,
133 implements: Vec::new(),
134 });
135 }
136 self.extract_function(node, content, false)
137 }
138
139 fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
140 None
141 }
142
143 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
144 let mut prev = node.prev_sibling();
146 let mut doc_lines = Vec::new();
147
148 while let Some(sibling) = prev {
149 let text = &content[sibling.byte_range()];
150 if sibling.kind() == "comment" && text.starts_with('"') {
151 let line = text.strip_prefix('"').unwrap_or(text).trim();
152 doc_lines.push(line.to_string());
153 prev = sibling.prev_sibling();
154 } else {
155 break;
156 }
157 }
158
159 if doc_lines.is_empty() {
160 return None;
161 }
162
163 doc_lines.reverse();
164 Some(doc_lines.join(" "))
165 }
166
167 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
168 Vec::new()
169 }
170
171 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
172 let text = &content[node.byte_range()];
173 let line = node.start_position().row + 1;
174
175 let module = if let Some(rest) = text.strip_prefix("source ") {
177 Some(rest.trim().to_string())
178 } else if let Some(rest) = text.strip_prefix("runtime ") {
179 Some(rest.trim().to_string())
180 } else {
181 None
182 };
183
184 if let Some(module) = module {
185 return vec![Import {
186 module,
187 names: Vec::new(),
188 alias: None,
189 is_wildcard: false,
190 is_relative: true,
191 line,
192 }];
193 }
194
195 Vec::new()
196 }
197
198 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
199 if import.is_relative {
201 format!("source {}", import.module)
202 } else {
203 format!("runtime {}", import.module)
204 }
205 }
206
207 fn is_public(&self, node: &Node, content: &str) -> bool {
208 self.node_name(node, content)
209 .map_or(true, |n| !n.starts_with("s:"))
210 }
211
212 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
213 if self.is_public(node, content) {
214 Visibility::Public
215 } else {
216 Visibility::Private
217 }
218 }
219
220 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
221 let name = symbol.name.as_str();
222 match symbol.kind {
223 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
224 crate::SymbolKind::Module => name == "tests" || name == "test",
225 _ => false,
226 }
227 }
228
229 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
230 None
231 }
232
233 fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
234 None
235 }
236 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
237 false
238 }
239
240 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
241 node.child_by_field_name("name")
242 .map(|n| &content[n.byte_range()])
243 }
244
245 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
246 let name = path.file_name()?.to_str()?;
247 if name.ends_with(".vim") || name == ".vimrc" || name == "vimrc" {
248 let stem = path.file_stem()?.to_str()?;
249 return Some(stem.to_string());
250 }
251 None
252 }
253
254 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
255 vec![
256 format!("{}.vim", module),
257 format!("plugin/{}.vim", module),
258 format!("autoload/{}.vim", module),
259 ]
260 }
261
262 fn lang_key(&self) -> &'static str {
263 "vim"
264 }
265
266 fn is_stdlib_import(&self, _import_name: &str, _project_root: &Path) -> bool {
267 false
268 }
269 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
270 None
271 }
272
273 fn resolve_local_import(
274 &self,
275 import: &str,
276 current_file: &Path,
277 _project_root: &Path,
278 ) -> Option<PathBuf> {
279 let dir = current_file.parent()?;
280 let full = dir.join(import);
281 if full.is_file() { Some(full) } else { None }
282 }
283
284 fn resolve_external_import(
285 &self,
286 _import_name: &str,
287 _project_root: &Path,
288 ) -> Option<ResolvedPackage> {
289 None
290 }
291
292 fn get_version(&self, _project_root: &Path) -> Option<String> {
293 None
294 }
295 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
296 None
297 }
298 fn indexable_extensions(&self) -> &'static [&'static str] {
299 &["vim"]
300 }
301 fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
302 Vec::new()
303 }
304
305 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
306 use crate::traits::{has_extension, skip_dotfiles};
307 if skip_dotfiles(name) {
308 return true;
309 }
310 !is_dir && !has_extension(name, self.indexable_extensions())
311 }
312
313 fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
314 Vec::new()
315 }
316
317 fn package_module_name(&self, entry_name: &str) -> String {
318 entry_name
319 .strip_suffix(".vim")
320 .unwrap_or(entry_name)
321 .to_string()
322 }
323
324 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
325 if path.is_file() {
326 Some(path.to_path_buf())
327 } else {
328 None
329 }
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::validate_unused_kinds_audit;
337
338 #[test]
339 fn unused_node_kinds_audit() {
340 #[rustfmt::skip]
341 let documented_unused: &[&str] = &[
342 "aboveleft_statement", "autocmd_statement", "augroup_statement",
343 "bang_filter_statement", "belowright_statement", "body", "botright_statement",
344 "break_statement", "call_expression", "call_statement", "catch_statement",
345 "cnext_statement", "colorscheme_statement", "comclear_statement",
346 "command_statement", "const_statement", "continue_statement", "cprevious_statement",
347 "delcommand_statement", "dictionnary_entry", "echo_statement", "echoerr_statement",
348 "echohl_statement", "echomsg_statement", "echon_statement", "edit_statement",
349 "else_statement", "enew_statement", "eval_statement", "ex_statement",
350 "execute_statement", "field_expression", "file_format", "filetype",
351 "filetype_statement", "filetypes", "finally_statement", "find_statement",
352 "function_declaration", "global_statement", "highlight_statement", "identifier",
353 "index_expression", "lambda_expression", "let_statement", "lua_statement",
354 "map_statement", "marker_definition", "match_case", "method_expression",
355 "normal_statement", "options_statement", "perl_statement", "python_statement",
356 "range_statement", "register_statement", "return_statement", "ruby_statement",
357 "scoped_identifier", "scriptencoding_statement", "set_statement",
358 "setfiletype_statement", "setlocal_statement", "sign_statement", "silent_statement",
359 "slice_expression", "startinsert_statement", "stopinsert_statement",
360 "substitute_statement", "syntax_statement", "ternary_expression",
361 "throw_statement", "topleft_statement", "unknown_builtin_statement",
362 "unlet_statement", "vertical_statement", "view_statement", "visual_statement",
363 "wincmd_statement",
364 ];
365 validate_unused_kinds_audit(&Vim, documented_unused)
366 .expect("Vim unused node kinds audit failed");
367 }
368}