1use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6pub struct Perl;
8
9impl Language for Perl {
10 fn name(&self) -> &'static str {
11 "Perl"
12 }
13 fn extensions(&self) -> &'static [&'static str] {
14 &["pl", "pm", "t"]
15 }
16 fn grammar_name(&self) -> &'static str {
17 "perl"
18 }
19
20 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21 Some(self)
22 }
23
24 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
25 let text = &content[node.byte_range()];
26 let line = node.start_position().row + 1;
27
28 let module = if let Some(rest) = text
31 .strip_prefix("use ")
32 .or_else(|| text.strip_prefix("require "))
33 {
34 rest.split([';', ' ']).next()
35 } else {
36 None
37 };
38
39 if let Some(module) = module {
40 let module = module.trim().to_string();
41 return vec![Import {
42 module: module.clone(),
43 names: Vec::new(),
44 alias: None,
45 is_wildcard: false,
46 is_relative: false,
47 line,
48 }];
49 }
50
51 Vec::new()
52 }
53
54 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
55 let names_to_use: Vec<&str> = names
57 .map(|n| n.to_vec())
58 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
59 if names_to_use.is_empty() {
60 format!("use {};", import.module)
61 } else {
62 format!("use {} qw({});", import.module, names_to_use.join(" "))
63 }
64 }
65
66 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
67 if self
68 .node_name(node, content)
69 .is_none_or(|n| !n.starts_with('_'))
70 {
71 Visibility::Public
72 } else {
73 Visibility::Private
74 }
75 }
76
77 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
78 let name = symbol.name.as_str();
79 match symbol.kind {
80 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
81 crate::SymbolKind::Module => name == "tests" || name == "test",
82 _ => false,
83 }
84 }
85
86 fn test_file_globs(&self) -> &'static [&'static str] {
87 &["**/t/**/*.t", "**/*.t"]
88 }
89
90 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
91 node.child_by_field_name("body")
92 }
93
94 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
95 let mut doc_lines: Vec<String> = Vec::new();
96 let mut prev = node.prev_sibling();
97
98 while let Some(sibling) = prev {
99 if sibling.kind() == "comment" || sibling.kind() == "comments" {
100 let text = &content[sibling.byte_range()];
101 if let Some(line) = text.strip_prefix('#') {
102 let line = line.strip_prefix(' ').unwrap_or(line);
103 doc_lines.push(line.to_string());
104 } else {
105 break;
106 }
107 } else {
108 break;
109 }
110 prev = sibling.prev_sibling();
111 }
112
113 if doc_lines.is_empty() {
114 return None;
115 }
116
117 doc_lines.reverse();
118 let joined = doc_lines.join("\n").trim().to_string();
119 if joined.is_empty() {
120 None
121 } else {
122 Some(joined)
123 }
124 }
125
126 fn analyze_container_body(
127 &self,
128 body_node: &Node,
129 content: &str,
130 inner_indent: &str,
131 ) -> Option<ContainerBody> {
132 crate::body::analyze_brace_body(body_node, content, inner_indent)
133 }
134}
135
136impl LanguageSymbols for Perl {}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::validate_unused_kinds_audit;
142
143 #[test]
144 fn unused_node_kinds_audit() {
145 #[rustfmt::skip]
146 let documented_unused: &[&str] = &[
147 "ambiguous_function_call_expression", "amper_deref_expression",
148 "anonymous_array_expression", "anonymous_hash_expression",
149 "anonymous_method_expression", "anonymous_slice_expression",
150 "anonymous_subroutine_expression", "array_deref_expression",
151 "array_element_expression", "arraylen_deref_expression", "assignment_expression",
152 "await_expression", "binary_expression", "block_statement", "class_phaser_statement",
153 "class_statement", "coderef_call_expression",
154 "defer_statement", "do_expression", "else", "elsif",
155 "equality_expression", "eval_expression", "expression_statement",
156 "fileglob_expression", "func0op_call_expression", "func1op_call_expression",
157 "function", "function_call_expression", "glob_deref_expression",
158 "glob_slot_expression", "goto_expression", "hash_deref_expression",
159 "hash_element_expression", "identifier", "keyval_expression",
160 "list_expression", "localization_expression",
161 "loopex_expression", "lowprec_logical_expression", "map_grep_expression",
162 "match_regexp", "match_regexp_modifiers", "method", "method_call_expression",
163 "method_declaration_statement", "phaser_statement", "postfix_conditional_expression",
164 "postfix_for_expression", "postfix_loop_expression", "postinc_expression",
165 "preinc_expression", "prototype", "quoted_regexp_modifiers", "readline_expression",
166 "refgen_expression", "relational_expression",
167 "require_version_expression", "return_expression", "role_statement",
168 "scalar_deref_expression", "slice_expression", "sort_expression", "statement_label",
169 "stub_expression", "substitution_regexp_modifiers", "transliteration_expression",
170 "transliteration_modifiers", "try_statement", "unary_expression", "undef_expression",
171 "use_version_statement", "variable_declaration",
172 "for_statement",
174 "conditional_statement",
175 "loop_statement",
176 "cstyle_for_statement",
177 "require_expression",
178 "block",
179 "use_statement",
180 "conditional_expression",
181 ];
182 validate_unused_kinds_audit(&Perl, documented_unused)
183 .expect("Perl unused node kinds audit failed");
184 }
185}