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 Perl;
10
11impl Language for Perl {
12 fn name(&self) -> &'static str {
13 "Perl"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["pl", "pm", "t"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "perl"
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 let text = &content[node.byte_range()];
28 let line = node.start_position().row + 1;
29
30 let module = if let Some(rest) = text
33 .strip_prefix("use ")
34 .or_else(|| text.strip_prefix("require "))
35 {
36 rest.split([';', ' ']).next()
37 } else {
38 None
39 };
40
41 if let Some(module) = module {
42 let module = module.trim().to_string();
43 return vec![Import {
44 module: module.clone(),
45 names: Vec::new(),
46 alias: None,
47 is_wildcard: false,
48 is_relative: false,
49 line,
50 }];
51 }
52
53 Vec::new()
54 }
55
56 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
57 let names_to_use: Vec<&str> = names
59 .map(|n| n.to_vec())
60 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
61 if names_to_use.is_empty() {
62 format!("use {};", import.module)
63 } else {
64 format!("use {} qw({});", import.module, names_to_use.join(" "))
65 }
66 }
67
68 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
69 if self
70 .node_name(node, content)
71 .is_none_or(|n| !n.starts_with('_'))
72 {
73 Visibility::Public
74 } else {
75 Visibility::Private
76 }
77 }
78
79 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
80 let name = symbol.name.as_str();
81 match symbol.kind {
82 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
83 crate::SymbolKind::Module => name == "tests" || name == "test",
84 _ => false,
85 }
86 }
87
88 fn test_file_globs(&self) -> &'static [&'static str] {
89 &["**/t/**/*.t", "**/*.t"]
90 }
91
92 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
93 node.child_by_field_name("body")
94 }
95
96 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
97 let mut doc_lines: Vec<String> = Vec::new();
98 let mut prev = node.prev_sibling();
99
100 while let Some(sibling) = prev {
101 if sibling.kind() == "comment" || sibling.kind() == "comments" {
102 let text = &content[sibling.byte_range()];
103 if let Some(line) = text.strip_prefix('#') {
104 let line = line.strip_prefix(' ').unwrap_or(line);
105 doc_lines.push(line.to_string());
106 } else {
107 break;
108 }
109 } else {
110 break;
111 }
112 prev = sibling.prev_sibling();
113 }
114
115 if doc_lines.is_empty() {
116 return None;
117 }
118
119 doc_lines.reverse();
120 let joined = doc_lines.join("\n").trim().to_string();
121 if joined.is_empty() {
122 None
123 } else {
124 Some(joined)
125 }
126 }
127
128 fn analyze_container_body(
129 &self,
130 body_node: &Node,
131 content: &str,
132 inner_indent: &str,
133 ) -> Option<ContainerBody> {
134 crate::body::analyze_brace_body(body_node, content, inner_indent)
135 }
136
137 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
138 static RESOLVER: PerlModuleResolver = PerlModuleResolver;
139 Some(&RESOLVER)
140 }
141}
142
143impl LanguageSymbols for Perl {}
144
145pub struct PerlModuleResolver;
153
154impl ModuleResolver for PerlModuleResolver {
155 fn workspace_config(&self, root: &Path) -> ResolverConfig {
156 ResolverConfig {
157 workspace_root: root.to_path_buf(),
158 path_mappings: Vec::new(),
159 search_roots: vec![root.join("lib"), root.to_path_buf()],
160 }
161 }
162
163 fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
164 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
165 if ext != "pm" {
166 return Vec::new();
167 }
168 for search_root in &cfg.search_roots {
169 if let Ok(rel) = file.strip_prefix(search_root) {
170 let module = rel
171 .to_str()
172 .unwrap_or("")
173 .trim_end_matches(".pm")
174 .replace('/', "::");
175 if !module.is_empty() {
176 return vec![ModuleId {
177 canonical_path: module,
178 }];
179 }
180 }
181 }
182 Vec::new()
183 }
184
185 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
186 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
187 if ext != "pl" && ext != "pm" && ext != "t" {
188 return Resolution::NotApplicable;
189 }
190 let raw = &spec.raw;
191 let path_part = raw.replace("::", "/");
192 let exported_name = raw.rsplit("::").next().unwrap_or(raw).to_string();
193
194 for search_root in &cfg.search_roots {
195 let candidate = search_root.join(format!("{}.pm", path_part));
196 if candidate.exists() {
197 return Resolution::Resolved(candidate, exported_name);
198 }
199 }
200 Resolution::NotFound
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::validate_unused_kinds_audit;
208
209 #[test]
210 fn unused_node_kinds_audit() {
211 #[rustfmt::skip]
212 let documented_unused: &[&str] = &[
213 "ambiguous_function_call_expression", "amper_deref_expression",
214 "anonymous_array_expression", "anonymous_hash_expression",
215 "anonymous_method_expression", "anonymous_slice_expression",
216 "anonymous_subroutine_expression", "array_deref_expression",
217 "array_element_expression", "arraylen_deref_expression", "assignment_expression",
218 "await_expression", "binary_expression", "block_statement", "class_phaser_statement",
219 "class_statement", "coderef_call_expression",
220 "defer_statement", "do_expression", "else", "elsif",
221 "equality_expression", "eval_expression", "expression_statement",
222 "fileglob_expression", "func0op_call_expression", "func1op_call_expression",
223 "function", "function_call_expression", "glob_deref_expression",
224 "glob_slot_expression", "goto_expression", "hash_deref_expression",
225 "hash_element_expression", "identifier", "keyval_expression",
226 "list_expression", "localization_expression",
227 "loopex_expression", "lowprec_logical_expression", "map_grep_expression",
228 "match_regexp", "match_regexp_modifiers", "method", "method_call_expression",
229 "method_declaration_statement", "phaser_statement", "postfix_conditional_expression",
230 "postfix_for_expression", "postfix_loop_expression", "postinc_expression",
231 "preinc_expression", "prototype", "quoted_regexp_modifiers", "readline_expression",
232 "refgen_expression", "relational_expression",
233 "require_version_expression", "return_expression", "role_statement",
234 "scalar_deref_expression", "slice_expression", "sort_expression", "statement_label",
235 "stub_expression", "substitution_regexp_modifiers", "transliteration_expression",
236 "transliteration_modifiers", "try_statement", "unary_expression", "undef_expression",
237 "use_version_statement", "variable_declaration",
238 "for_statement",
240 "conditional_statement",
241 "loop_statement",
242 "cstyle_for_statement",
243 "require_expression",
244 "block",
245 "use_statement",
246 "conditional_expression",
247 ];
248 validate_unused_kinds_audit(&Perl, documented_unused)
249 .expect("Perl unused node kinds audit failed");
250 }
251}