1use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6pub 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 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 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 Visibility::Public
199 }
200}
201
202impl LanguageSymbols for Php {}
203
204fn 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 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 "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 "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 "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 "echo_statement", "empty_statement", "exit_statement", "expression_statement",
274 "global_declaration", "goto_statement", "named_label_statement", "unset_statement",
275 "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}