1use crate::external_packages::ResolvedPackage;
4use crate::{
5 Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism,
6 simple_function_symbol,
7};
8use std::path::{Path, PathBuf};
9use tree_sitter::Node;
10
11pub struct PowerShell;
13
14impl Language for PowerShell {
15 fn name(&self) -> &'static str {
16 "PowerShell"
17 }
18 fn extensions(&self) -> &'static [&'static str] {
19 &["ps1", "psm1", "psd1"]
20 }
21 fn grammar_name(&self) -> &'static str {
22 "powershell"
23 }
24
25 fn has_symbols(&self) -> bool {
26 true
27 }
28
29 fn container_kinds(&self) -> &'static [&'static str] {
30 &["class_statement"]
31 }
32
33 fn function_kinds(&self) -> &'static [&'static str] {
34 &["function_statement"]
35 }
36
37 fn type_kinds(&self) -> &'static [&'static str] {
38 &["class_statement", "enum_statement"]
39 }
40
41 fn import_kinds(&self) -> &'static [&'static str] {
42 &["pipeline"] }
44
45 fn public_symbol_kinds(&self) -> &'static [&'static str] {
46 &["function_statement", "class_statement"]
47 }
48
49 fn visibility_mechanism(&self) -> VisibilityMechanism {
50 VisibilityMechanism::AllPublic
51 }
52
53 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
54 let name = match self.node_name(node, content) {
55 Some(n) => n.to_string(),
56 None => return Vec::new(),
57 };
58
59 let kind = match node.kind() {
60 "function_statement" => SymbolKind::Function,
61 "class_statement" => SymbolKind::Class,
62 _ => return Vec::new(),
63 };
64
65 vec![Export {
66 name,
67 kind,
68 line: node.start_position().row + 1,
69 }]
70 }
71
72 fn scope_creating_kinds(&self) -> &'static [&'static str] {
73 &["function_statement", "class_statement", "script_block"]
74 }
75
76 fn control_flow_kinds(&self) -> &'static [&'static str] {
77 &[
78 "if_statement",
79 "while_statement",
80 "for_statement",
81 "foreach_statement",
82 "switch_statement",
83 "try_statement",
84 ]
85 }
86
87 fn complexity_nodes(&self) -> &'static [&'static str] {
88 &[
89 "if_statement",
90 "elseif_clause",
91 "while_statement",
92 "for_statement",
93 "foreach_statement",
94 "switch_statement",
95 "catch_clause",
96 ]
97 }
98
99 fn nesting_nodes(&self) -> &'static [&'static str] {
100 &[
101 "function_statement",
102 "class_statement",
103 "if_statement",
104 "while_statement",
105 "for_statement",
106 "try_statement",
107 ]
108 }
109
110 fn signature_suffix(&self) -> &'static str {
111 ""
112 }
113
114 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
115 let name = self.node_name(node, content)?;
116 Some(simple_function_symbol(
117 node,
118 content,
119 name,
120 self.extract_docstring(node, content),
121 ))
122 }
123
124 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
125 if node.kind() != "class_statement" {
126 return None;
127 }
128
129 let name = self.node_name(node, content)?;
130 let text = &content[node.byte_range()];
131 let first_line = text.lines().next().unwrap_or(text);
132
133 Some(Symbol {
134 name: name.to_string(),
135 kind: SymbolKind::Class,
136 signature: first_line.trim().to_string(),
137 docstring: self.extract_docstring(node, content),
138 attributes: Vec::new(),
139 start_line: node.start_position().row + 1,
140 end_line: node.end_position().row + 1,
141 visibility: Visibility::Public,
142 children: Vec::new(),
143 is_interface_impl: false,
144 implements: Vec::new(),
145 })
146 }
147
148 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
149 let name = self.node_name(node, content)?;
150 let kind = match node.kind() {
151 "class_statement" => SymbolKind::Class,
152 "enum_statement" => SymbolKind::Enum,
153 _ => return None,
154 };
155
156 Some(Symbol {
157 name: name.to_string(),
158 kind,
159 signature: format!("{} {}", node.kind().replace("_statement", ""), name),
160 docstring: self.extract_docstring(node, content),
161 attributes: Vec::new(),
162 start_line: node.start_position().row + 1,
163 end_line: node.end_position().row + 1,
164 visibility: Visibility::Public,
165 children: Vec::new(),
166 is_interface_impl: false,
167 implements: Vec::new(),
168 })
169 }
170
171 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
172 let mut prev = node.prev_sibling();
174 while let Some(sibling) = prev {
175 let text = &content[sibling.byte_range()];
176 if sibling.kind() == "comment" {
177 if text.starts_with("<#") {
178 let inner = text.trim_start_matches("<#").trim_end_matches("#>").trim();
179 if !inner.is_empty() {
180 return Some(inner.lines().next().unwrap_or(inner).to_string());
181 }
182 } else if text.starts_with('#') {
183 let line = text.strip_prefix('#').unwrap_or(text).trim();
184 return Some(line.to_string());
185 }
186 }
187 prev = sibling.prev_sibling();
188 }
189 None
190 }
191
192 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
193 Vec::new()
194 }
195
196 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
197 if node.kind() != "pipeline" {
198 return Vec::new();
199 }
200
201 let text = &content[node.byte_range()];
202 let line = node.start_position().row + 1;
203
204 if let Some(rest) = text.strip_prefix("Import-Module ") {
206 let module = rest.split_whitespace().next().map(|s| s.to_string());
207 if let Some(module) = module {
208 return vec![Import {
209 module,
210 names: Vec::new(),
211 alias: None,
212 is_wildcard: true,
213 is_relative: false,
214 line,
215 }];
216 }
217 }
218
219 Vec::new()
220 }
221
222 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
223 format!("Import-Module {}", import.module)
225 }
226
227 fn is_public(&self, _node: &Node, _content: &str) -> bool {
228 true
229 }
230 fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
231 Visibility::Public
232 }
233
234 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
235 let name = symbol.name.as_str();
236 match symbol.kind {
237 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
238 crate::SymbolKind::Module => name == "tests" || name == "test",
239 _ => false,
240 }
241 }
242
243 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
244 None
245 }
246
247 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
248 node.child_by_field_name("body")
249 }
250
251 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
252 false
253 }
254
255 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
256 node.child_by_field_name("name")
257 .map(|n| &content[n.byte_range()])
258 }
259
260 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
261 let ext = path.extension()?.to_str()?;
262 if !["ps1", "psm1", "psd1"].contains(&ext) {
263 return None;
264 }
265 let stem = path.file_stem()?.to_str()?;
266 Some(stem.to_string())
267 }
268
269 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
270 vec![format!("{}.psm1", module), format!("{}.ps1", module)]
271 }
272
273 fn lang_key(&self) -> &'static str {
274 "powershell"
275 }
276
277 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
278 matches!(
279 import_name,
280 "Microsoft.PowerShell.Core"
281 | "Microsoft.PowerShell.Utility"
282 | "Microsoft.PowerShell.Management"
283 | "Microsoft.PowerShell.Security"
284 )
285 }
286
287 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
288 None
289 }
290 fn resolve_local_import(&self, import: &str, _: &Path, project_root: &Path) -> Option<PathBuf> {
291 let full = project_root.join(format!("{}.psm1", import));
292 if full.is_file() { Some(full) } else { None }
293 }
294 fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
295 None
296 }
297
298 fn get_version(&self, project_root: &Path) -> Option<String> {
299 for entry in std::fs::read_dir(project_root).ok()? {
301 let entry = entry.ok()?;
302 let path = entry.path();
303 if path.extension().map_or(false, |e| e == "psd1") {
304 return Some("PowerShell Module".to_string());
305 }
306 }
307 None
308 }
309
310 fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
311 if let Some(home) = std::env::var_os("HOME") {
312 let modules = PathBuf::from(home).join(".local/share/powershell/Modules");
313 if modules.is_dir() {
314 return Some(modules);
315 }
316 }
317 None
318 }
319
320 fn indexable_extensions(&self) -> &'static [&'static str] {
321 &["ps1", "psm1"]
322 }
323 fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
324 Vec::new()
325 }
326
327 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
328 use crate::traits::{has_extension, skip_dotfiles};
329 if skip_dotfiles(name) {
330 return true;
331 }
332 !is_dir && !has_extension(name, self.indexable_extensions())
333 }
334
335 fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
336 Vec::new()
337 }
338
339 fn package_module_name(&self, entry_name: &str) -> String {
340 entry_name
341 .strip_suffix(".psm1")
342 .or_else(|| entry_name.strip_suffix(".ps1"))
343 .or_else(|| entry_name.strip_suffix(".psd1"))
344 .unwrap_or(entry_name)
345 .to_string()
346 }
347
348 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
349 if path.is_file() {
350 return Some(path.to_path_buf());
351 }
352 if path.is_dir() {
354 let name = path.file_name()?.to_str()?;
355 let module = path.join(format!("{}.psm1", name));
356 if module.is_file() {
357 return Some(module);
358 }
359 }
360 None
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use crate::validate_unused_kinds_audit;
368
369 #[test]
370 fn unused_node_kinds_audit() {
371 #[rustfmt::skip]
372 let documented_unused: &[&str] = &[
373 "additive_argument_expression", "additive_expression", "argument_expression",
374 "argument_expression_list", "array_expression", "array_literal_expression",
375 "array_type_name", "assignement_operator", "assignment_expression",
376 "bitwise_argument_expression", "bitwise_expression", "block_name", "cast_expression",
377 "catch_clauses", "catch_type_list", "class_attribute", "class_method_definition",
378 "class_method_parameter", "class_method_parameter_list", "class_property_definition",
379 "command_invokation_operator", "comparison_argument_expression",
380 "comparison_expression", "comparison_operator", "data_statement", "do_statement",
381 "else_clause", "elseif_clauses", "empty_statement", "enum_member",
382 "expression_with_unary_operator", "file_redirection_operator", "finally_clause",
383 "flow_control_statement", "for_condition", "for_initializer", "for_iterator",
384 "foreach_command", "foreach_parameter", "format_argument_expression",
385 "format_expression", "format_operator", "function_name",
386 "function_parameter_declaration", "generic_type_arguments", "generic_type_name",
387 "hash_entry", "hash_literal_body", "hash_literal_expression",
388 "inlinescript_statement", "invokation_expression", "invokation_foreach_expression",
389 "key_expression", "label_expression", "left_assignment_expression",
390 "logical_argument_expression", "logical_expression", "merging_redirection_operator",
391 "multiplicative_argument_expression", "multiplicative_expression", "named_block",
392 "named_block_list", "parallel_statement", "param_block", "parenthesized_expression",
393 "post_decrement_expression", "post_increment_expression", "pre_decrement_expression",
394 "pre_increment_expression", "range_argument_expression", "range_expression",
395 "script_block_body", "script_block_expression", "sequence_statement",
396 "statement_block", "statement_list", "sub_expression", "switch_body",
397 "switch_clause", "switch_clause_condition", "switch_clauses", "trap_statement",
398 "type_identifier", "type_literal", "type_name", "type_spec", "unary_expression",
399 "while_condition",
400 ];
401 validate_unused_kinds_audit(&PowerShell, documented_unused)
402 .expect("PowerShell unused node kinds audit failed");
403 }
404}