Skip to main content

normalize_languages/
php.rs

1//! PHP language support.
2
3use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6/// PHP language support.
7pub struct Php;
8
9impl Language for Php {
10    fn name(&self) -> &'static str {
11        "PHP"
12    }
13    fn extensions(&self) -> &'static [&'static str] {
14        &["php", "phtml"]
15    }
16    fn grammar_name(&self) -> &'static str {
17        "php"
18    }
19
20    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21        Some(self)
22    }
23
24    fn signature_suffix(&self) -> &'static str {
25        " {}"
26    }
27
28    fn refine_kind(
29        &self,
30        node: &Node,
31        _content: &str,
32        tag_kind: crate::SymbolKind,
33    ) -> crate::SymbolKind {
34        match node.kind() {
35            "enum_declaration" => crate::SymbolKind::Enum,
36            "interface_declaration" => crate::SymbolKind::Interface,
37            "trait_declaration" => crate::SymbolKind::Trait,
38            _ => tag_kind,
39        }
40    }
41
42    fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
43        let mut attrs = Vec::new();
44        let mut cursor = node.walk();
45        for child in node.children(&mut cursor) {
46            if child.kind() == "attribute_list" {
47                let mut ac = child.walk();
48                for attr in child.children(&mut ac) {
49                    if attr.kind() == "attribute_group" || attr.kind() == "attribute" {
50                        attrs.push(content[attr.byte_range()].to_string());
51                    }
52                }
53            }
54        }
55        // Also check preceding siblings (PHP attributes can precede the declaration)
56        let mut prev = node.prev_sibling();
57        while let Some(sibling) = prev {
58            if sibling.kind() == "attribute_list" {
59                let mut ac = sibling.walk();
60                for attr in sibling.children(&mut ac) {
61                    if attr.kind() == "attribute_group" || attr.kind() == "attribute" {
62                        attrs.push(content[attr.byte_range()].to_string());
63                    }
64                }
65                prev = sibling.prev_sibling();
66                continue;
67            }
68            break;
69        }
70        attrs
71    }
72
73    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
74        extract_phpdoc(node, content)
75    }
76
77    fn build_signature(&self, node: &Node, content: &str) -> String {
78        let name = match self.node_name(node, content) {
79            Some(n) => n,
80            None => {
81                let text = &content[node.byte_range()];
82                return text.lines().next().unwrap_or(text).trim().to_string();
83            }
84        };
85        match node.kind() {
86            "function_declaration" | "method_declaration" => {
87                let params = node
88                    .child_by_field_name("parameters")
89                    .map(|p| content[p.byte_range()].to_string())
90                    .unwrap_or_else(|| "()".to_string());
91                let return_type = node
92                    .child_by_field_name("return_type")
93                    .map(|t| format!(": {}", content[t.byte_range()].trim()))
94                    .unwrap_or_default();
95                format!("function {}{}{}", name, params, return_type)
96            }
97            "interface_declaration" => format!("interface {}", name),
98            "trait_declaration" => format!("trait {}", name),
99            "enum_declaration" => format!("enum {}", name),
100            "namespace_definition" => format!("namespace {}", name),
101            _ => format!("class {}", name),
102        }
103    }
104
105    fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
106        let mut implements = Vec::new();
107        let mut cursor = node.walk();
108        for child in node.children(&mut cursor) {
109            if child.kind() == "base_clause" || child.kind() == "class_interface_clause" {
110                let mut cl = child.walk();
111                for t in child.children(&mut cl) {
112                    if t.kind() == "name" || t.kind() == "qualified_name" {
113                        implements.push(content[t.byte_range()].to_string());
114                    }
115                }
116            }
117        }
118        crate::ImplementsInfo {
119            is_interface: node.kind() == "interface_declaration",
120            implements,
121        }
122    }
123
124    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
125        if node.kind() != "namespace_use_declaration" {
126            return Vec::new();
127        }
128
129        let line = node.start_position().row + 1;
130        let mut imports = Vec::new();
131
132        let mut cursor = node.walk();
133        for child in node.children(&mut cursor) {
134            if child.kind() == "namespace_use_clause" {
135                let text = content[child.byte_range()].to_string();
136                imports.push(Import {
137                    module: text,
138                    names: Vec::new(),
139                    alias: None,
140                    is_wildcard: false,
141                    is_relative: false,
142                    line,
143                });
144            }
145        }
146
147        imports
148    }
149
150    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
151        // PHP: use Namespace\Class;
152        format!("use {};", import.module)
153    }
154
155    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
156        let name = symbol.name.as_str();
157        match symbol.kind {
158            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
159            crate::SymbolKind::Module => name == "tests" || name == "test",
160            _ => false,
161        }
162    }
163
164    fn test_file_globs(&self) -> &'static [&'static str] {
165        &["**/*Test.php"]
166    }
167
168    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
169        node.child_by_field_name("body")
170    }
171
172    fn analyze_container_body(
173        &self,
174        body_node: &Node,
175        content: &str,
176        inner_indent: &str,
177    ) -> Option<ContainerBody> {
178        crate::body::analyze_brace_body(body_node, content, inner_indent)
179    }
180
181    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
182        let mut cursor = node.walk();
183        for child in node.children(&mut cursor) {
184            if child.kind() == "visibility_modifier" {
185                let mod_text = &content[child.byte_range()];
186                if mod_text == "private" {
187                    return Visibility::Private;
188                }
189                if mod_text == "protected" {
190                    return Visibility::Protected;
191                }
192                if mod_text == "public" {
193                    return Visibility::Public;
194                }
195            }
196        }
197        // PHP default visibility for methods/properties in classes is public
198        Visibility::Public
199    }
200}
201
202impl LanguageSymbols for Php {}
203
204/// Extract a PHPDoc comment (`/** ... */`) preceding a PHP declaration.
205fn extract_phpdoc(node: &Node, content: &str) -> Option<String> {
206    let mut prev = node.prev_sibling();
207    while let Some(sibling) = prev {
208        if sibling.kind() == "comment" {
209            let text = &content[sibling.byte_range()];
210            if text.starts_with("/**") {
211                let lines: Vec<&str> = text
212                    .strip_prefix("/**")
213                    .unwrap_or(text)
214                    .strip_suffix("*/")
215                    .unwrap_or(text)
216                    .lines()
217                    .map(|l| l.trim().strip_prefix('*').unwrap_or(l).trim())
218                    .filter(|l| !l.is_empty())
219                    .collect();
220                if !lines.is_empty() {
221                    return Some(lines.join(" "));
222                }
223            }
224            return None;
225        }
226        // Skip attributes between doc comment and declaration
227        if sibling.kind() == "attribute_list" {
228            prev = sibling.prev_sibling();
229            continue;
230        }
231        return None;
232    }
233    None
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::validate_unused_kinds_audit;
240
241    #[test]
242    fn unused_node_kinds_audit() {
243        #[rustfmt::skip]
244        let documented_unused: &[&str] = &[
245            // STRUCTURAL
246            "abstract_modifier", "anonymous_class", "anonymous_function",
247            "anonymous_function_use_clause", "base_clause", "cast_expression", "cast_type",
248            "class_constant_access_expression", "class_interface_clause", "colon_block",
249            "compound_statement", "const_declaration", "declaration_list", "enum_case",
250            "enum_declaration_list", "final_modifier", "formal_parameters", "heredoc_body",
251            "named_type", "namespace_use_clause", "nowdoc_body",
252            "optional_type", "primitive_type", "property_declaration", "qualified_name",
253            "readonly_modifier", "reference_modifier", "static_modifier", "static_variable_declaration",
254            "use_as_clause", "use_declaration", "use_instead_of_clause", "var_modifier",
255            "visibility_modifier",
256            // CLAUSE
257            "declare_statement", "default_statement", "else_clause", "else_if_clause",
258            "finally_clause", "match_block", "match_condition_list", "match_conditional_expression",
259            "match_default_expression", "switch_block",
260            // EXPRESSION
261            "array_creation_expression", "assignment_expression", "augmented_assignment_expression",
262            "binary_expression", "bottom_type", "clone_expression", "disjunctive_normal_form_type",
263            "error_suppression_expression", "function_call_expression", "function_static_declaration",
264            "include_expression", "include_once_expression", "intersection_type",
265            "match_expression", "member_access_expression", "member_call_expression",
266            "nullsafe_member_access_expression", "nullsafe_member_call_expression",
267            "object_creation_expression", "parenthesized_expression", "reference_assignment_expression",
268            "require_expression", "require_once_expression", "scoped_call_expression",
269            "scoped_property_access_expression", "sequence_expression", "shell_command_expression",
270            "subscript_expression", "type_list", "unary_op_expression", "union_type",
271            "update_expression", "yield_expression",
272            // STATEMENT
273            "echo_statement", "empty_statement", "exit_statement", "expression_statement",
274            "global_declaration", "goto_statement", "named_label_statement", "unset_statement",
275            // control flow — not extracted as symbols
276            "do_statement",
277            "break_statement",
278            "arrow_function",
279            "if_statement",
280            "for_statement",
281            "return_statement",
282            "foreach_statement",
283            "case_statement",
284            "namespace_use_declaration",
285            "switch_statement",
286            "throw_expression",
287            "continue_statement",
288            "catch_clause",
289            "conditional_expression",
290            "while_statement",
291            "try_statement",
292        ];
293
294        validate_unused_kinds_audit(&Php, documented_unused)
295            .expect("PHP unused node kinds audit failed");
296    }
297}