Skip to main content

normalize_languages/
perl.rs

1//! Perl language support.
2
3use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6/// Perl language support.
7pub 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        // use Module::Name;
29        // require Module::Name;
30        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        // Perl: use Module; or use Module qw(a b c);
56        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            // control flow — not extracted as symbols
173            "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}