qlty_analysis/lang/
php.rs

1use crate::code::node_source;
2use crate::code::File;
3use crate::lang::Language;
4use anyhow::Context;
5use tree_sitter::Node;
6
7const CLASS_QUERY: &str = r#"
8[
9    (class_declaration
10        name: (name) @name)
11    (trait_declaration
12        name: (name) @name)
13    (interface_declaration
14        name: (name) @name)
15] @definition.class
16"#;
17
18const FUNCTION_DECLARATION_QUERY: &str = r#"
19[
20    (method_declaration
21        name: (name) @name
22        parameters: (_) @parameters)
23    (function_definition
24        name: (name) @name
25        parameters: (_) @parameters)
26    (assignment_expression
27        left:
28            name: (_) @name
29        right: [
30            (arrow_function
31                parameters: (_) @parameters)
32            (anonymous_function_creation_expression
33                parameters: (_) @parameters)
34        ])
35] @definition.function
36"#;
37
38const FIELD_QUERY: &str = r#"
39[
40    (class_declaration
41        name: (name)
42        body: (declaration_list
43            (property_declaration
44                (property_element
45                    (variable_name (_) @name)
46                    (property_initializer
47                    	(_) @field)
48                )
49            )
50        )
51    )
52    (assignment_expression
53      left: (member_access_expression
54          object: (_) @obj.name
55          name: (_) @name
56          (#eq? @obj.name "$this")
57      )
58      right: (_) @field
59    )
60    (member_access_expression
61        object: (_) @obj.name
62        name: (_) @name
63        (#eq? @obj.name "$this")
64    ) @field
65]
66"#;
67
68pub struct Php {
69    pub class_query: tree_sitter::Query,
70    pub function_declaration_query: tree_sitter::Query,
71    pub field_query: tree_sitter::Query,
72}
73
74impl Php {
75    pub const NAME: &'static str = "php";
76    pub const THIS: &'static str = "$this";
77
78    // Constants for different node types
79    pub const BINARY_EXPRESSION: &'static str = "binary_expression";
80    pub const BOOLEAN_OPERATOR: &'static str = "boolean_operator";
81    pub const CALL_EXPRESSION: &'static str = "function_call_expression";
82    pub const CASE_STATEMENT: &'static str = "case_statement";
83    pub const ARROW_FUNCTION: &'static str = "arrow_function";
84    pub const ANONYMOUS_FUNCTION: &'static str = "anonymous_function_creation_expression";
85    pub const COMMENT: &'static str = "comment";
86    pub const CONDITIONAL_ASSIGNMENT: &'static str = "conditional_assignment";
87    pub const CATCH_CLAUSE: &'static str = "catch_clause";
88    pub const ELSE_CLAUSE: &'static str = "else_clause";
89    pub const ELSE_IF_CLAUSE: &'static str = "else_if_clause";
90    pub const FUNCTION_DEFINITION: &'static str = "function_definition";
91    pub const METHOD_DECLARATION: &'static str = "method_declaration";
92    pub const IF_STATEMENT: &'static str = "if_statement";
93    pub const IDENTIFIER: &'static str = "identifier";
94    pub const WHILE_STATEMENT: &'static str = "while_statement";
95    pub const FOR_STATEMENT: &'static str = "for_statement";
96    pub const FOREACH_STATEMENT: &'static str = "foreach_statement";
97    pub const DO_WHILE_STATEMENT: &'static str = "do_statement";
98    pub const SWITCH_STATEMENT: &'static str = "switch_statement";
99    pub const RETURN_STATEMENT: &'static str = "return_statement";
100    pub const BREAK_STATEMENT: &'static str = "break_statement";
101    pub const CONTINUE_STATEMENT: &'static str = "continue_statement";
102    pub const TERNARY_EXPRESSION: &'static str = "conditional_assignment";
103    pub const TRY_STATEMENT: &'static str = "try_statement";
104    pub const STRING: &'static str = "string";
105    pub const ENCASPED_STRING: &'static str = "encapsed_string";
106    pub const PROGRAM: &'static str = "program";
107    pub const AND: &'static str = "&&";
108    pub const OR: &'static str = "||";
109    pub const CONSTRUCTOR_NAME: &'static str = "__construct";
110    pub const MEMBER_CALL_EXPRESSION: &'static str = "member_call_expression";
111    pub const MEMBER_ACCESS_EXPRESSION: &'static str = "member_access_expression";
112}
113
114impl Default for Php {
115    fn default() -> Self {
116        let language = tree_sitter_php::language_php();
117
118        Self {
119            class_query: tree_sitter::Query::new(&language, CLASS_QUERY).unwrap(),
120            field_query: tree_sitter::Query::new(&language, FIELD_QUERY).unwrap(),
121            function_declaration_query: tree_sitter::Query::new(
122                &language,
123                FUNCTION_DECLARATION_QUERY,
124            )
125            .unwrap(),
126        }
127    }
128}
129
130impl Language for Php {
131    fn name(&self) -> &str {
132        Self::NAME
133    }
134
135    fn self_keyword(&self) -> Option<&str> {
136        Some(Self::THIS)
137    }
138
139    fn binary_nodes(&self) -> Vec<&str> {
140        vec![Self::BINARY_EXPRESSION]
141    }
142
143    fn boolean_operator_nodes(&self) -> Vec<&str> {
144        vec![Self::AND, Self::OR]
145    }
146
147    fn call_nodes(&self) -> Vec<&str> {
148        vec![Self::CALL_EXPRESSION]
149    }
150
151    fn case_nodes(&self) -> Vec<&str> {
152        vec![Self::CASE_STATEMENT]
153    }
154
155    fn closure_nodes(&self) -> Vec<&str> {
156        vec![Self::ARROW_FUNCTION, Self::ANONYMOUS_FUNCTION]
157    }
158
159    fn comment_nodes(&self) -> Vec<&str> {
160        vec![Self::COMMENT]
161    }
162
163    fn conditional_assignment_nodes(&self) -> Vec<&str> {
164        vec![Self::CONDITIONAL_ASSIGNMENT]
165    }
166
167    fn else_nodes(&self) -> Vec<&str> {
168        vec![Self::ELSE_CLAUSE]
169    }
170
171    fn elsif_nodes(&self) -> Vec<&str> {
172        vec![Self::ELSE_IF_CLAUSE]
173    }
174
175    fn except_nodes(&self) -> Vec<&str> {
176        vec![Self::CATCH_CLAUSE]
177    }
178
179    fn field_nodes(&self) -> Vec<&str> {
180        vec![Self::MEMBER_CALL_EXPRESSION, Self::MEMBER_ACCESS_EXPRESSION]
181    }
182
183    fn function_nodes(&self) -> Vec<&str> {
184        vec![Self::FUNCTION_DEFINITION, Self::METHOD_DECLARATION]
185    }
186
187    fn if_nodes(&self) -> Vec<&str> {
188        vec![Self::IF_STATEMENT]
189    }
190
191    fn invisible_container_nodes(&self) -> Vec<&str> {
192        vec![Self::PROGRAM]
193    }
194
195    fn jump_nodes(&self) -> Vec<&str> {
196        vec![Self::BREAK_STATEMENT, Self::CONTINUE_STATEMENT]
197    }
198
199    fn loop_nodes(&self) -> Vec<&str> {
200        vec![
201            Self::WHILE_STATEMENT,
202            Self::FOR_STATEMENT,
203            Self::FOREACH_STATEMENT,
204            Self::DO_WHILE_STATEMENT,
205        ]
206    }
207
208    fn return_nodes(&self) -> Vec<&str> {
209        vec![Self::RETURN_STATEMENT]
210    }
211
212    fn string_nodes(&self) -> Vec<&str> {
213        vec![Self::STRING, Self::ENCASPED_STRING]
214    }
215
216    fn switch_nodes(&self) -> Vec<&str> {
217        vec![Self::SWITCH_STATEMENT]
218    }
219
220    fn ternary_nodes(&self) -> Vec<&str> {
221        vec![Self::TERNARY_EXPRESSION]
222    }
223
224    fn try_expression_nodes(&self) -> Vec<&str> {
225        vec![Self::TRY_STATEMENT]
226    }
227
228    fn constructor_names(&self) -> Vec<&str> {
229        vec![Self::CONSTRUCTOR_NAME]
230    }
231
232    fn call_identifiers(&self, source_file: &File, node: &Node) -> (Option<String>, String) {
233        let function_node = node.child(0).unwrap();
234        let function_kind = function_node.kind();
235
236        match function_kind {
237            Self::CALL_EXPRESSION => (
238                None,
239                node_source(&function_node.child(0).unwrap(), source_file),
240            ),
241            Self::MEMBER_CALL_EXPRESSION => {
242                let object = function_node.child(0).unwrap();
243                let object_name = node_source(&object, source_file);
244                let method = function_node.child(2).unwrap();
245                let method_name = node_source(&method, source_file);
246                (Some(object_name), method_name)
247            }
248            _ => (Some("<UNKNOWN>".to_string()), String::new()),
249        }
250    }
251
252    fn field_identifiers(&self, source_file: &File, node: &Node) -> (String, String) {
253        (
254            node_source(&node.child(0).unwrap(), source_file),
255            node_source(
256                &node
257                    .child_by_field_name("name")
258                    .with_context(|| format!("file_path: {:?}", source_file.path))
259                    .unwrap(),
260                source_file,
261            ),
262        )
263    }
264
265    fn tree_sitter_language(&self) -> tree_sitter::Language {
266        tree_sitter_php::language_php()
267    }
268
269    fn class_query(&self) -> &tree_sitter::Query {
270        &self.class_query
271    }
272
273    fn function_declaration_query(&self) -> &tree_sitter::Query {
274        &self.function_declaration_query
275    }
276
277    fn field_query(&self) -> &tree_sitter::Query {
278        &self.field_query
279    }
280}
281
282#[cfg(test)]
283mod test {
284    use super::*;
285    use std::collections::HashSet;
286    use tree_sitter::Tree;
287
288    #[test]
289    fn mutually_exclusive() {
290        let php = Php::default();
291        let mut kinds: Vec<&str> = vec![];
292
293        kinds.extend(php.binary_nodes());
294        kinds.extend(php.boolean_operator_nodes());
295        kinds.extend(php.call_nodes());
296        kinds.extend(php.case_nodes());
297        kinds.extend(php.closure_nodes());
298        kinds.extend(php.comment_nodes());
299        kinds.extend(php.conditional_assignment_nodes());
300        kinds.extend(php.else_nodes());
301        kinds.extend(php.except_nodes());
302        kinds.extend(php.field_nodes());
303        kinds.extend(php.function_nodes());
304        kinds.extend(php.if_nodes());
305        kinds.extend(php.invisible_container_nodes());
306        kinds.extend(php.jump_nodes());
307        kinds.extend(php.loop_nodes());
308        kinds.extend(php.return_nodes());
309        kinds.extend(php.string_nodes());
310        kinds.extend(php.switch_nodes());
311        kinds.extend(php.try_expression_nodes());
312
313        let unique: HashSet<_> = kinds.iter().cloned().collect();
314        assert_eq!(unique.len(), kinds.len());
315    }
316
317    #[test]
318    fn field_identifier_read() {
319        let source_file = File::from_string(Php::NAME, "<?php $foobar->foo;?>");
320        let tree = source_file.parse();
321        let root_node = tree.root_node();
322        let expression = root_node.named_child(1).unwrap();
323        let field = expression.named_child(0).unwrap();
324        let language = Php::default();
325
326        assert_eq!(
327            language.field_identifiers(&source_file, &field),
328            ("$foobar".to_string(), "foo".to_string())
329        );
330    }
331
332    #[test]
333    fn field_identifier_write() {
334        let source_file = File::from_string(Php::NAME, "<?php $foobar->foo = 1;?>");
335        let tree = source_file.parse();
336        let root_node = tree.root_node();
337        let expression = root_node.named_child(1).unwrap();
338        let field = expression.named_child(0).unwrap().named_child(0).unwrap();
339        let language = Php::default();
340
341        assert_eq!(
342            language.field_identifiers(&source_file, &field),
343            ("$foobar".to_string(), "foo".to_string())
344        );
345    }
346
347    #[test]
348    fn this_field_identifier_read() {
349        let source_file = File::from_string(Php::NAME, "<?php $this->foo;?>");
350        let tree = source_file.parse();
351        let root_node = tree.root_node();
352        let expression = root_node.named_child(1).unwrap();
353        let field = expression.named_child(0).unwrap();
354        let language = Php::default();
355
356        assert_eq!(
357            language.field_identifiers(&source_file, &field),
358            ("$this".to_string(), "foo".to_string())
359        );
360    }
361
362    #[test]
363    fn this_field_identifier_write() {
364        let source_file = File::from_string(Php::NAME, "<?php $this->foo = 1;?>");
365        let tree = source_file.parse();
366        let root_node = tree.root_node();
367        let expression = root_node.named_child(1).unwrap();
368        let field = expression.named_child(0).unwrap().named_child(0).unwrap();
369        let language = Php::default();
370
371        assert_eq!(
372            language.field_identifiers(&source_file, &field),
373            ("$this".to_string(), "foo".to_string())
374        );
375    }
376
377    #[test]
378    fn call_identifier() {
379        let source_file = File::from_string(Php::NAME, "<?php foo(); ?>");
380        let tree = source_file.parse();
381        let call = call_node(&tree);
382        let language = Php::default();
383
384        assert_eq!(
385            language.call_identifiers(&source_file, &call),
386            (None, "foo".to_string())
387        );
388    }
389
390    #[test]
391    fn call_attribute() {
392        let source_file = File::from_string(Php::NAME, "<?php $foo->bar() ?>");
393        let tree = source_file.parse();
394        let call = call_node(&tree);
395        let language = Php::default();
396
397        assert_eq!(
398            language.call_identifiers(&source_file, &call),
399            (Some("$foo".to_string()), "bar".to_string())
400        );
401    }
402
403    #[test]
404    fn this_attribute() {
405        let source_file = File::from_string(Php::NAME, "<?php $this->bar() ?>");
406        let tree = source_file.parse();
407        let call = call_node(&tree);
408        let language = Php::default();
409
410        assert_eq!(
411            language.call_identifiers(&source_file, &call),
412            (Some("$this".to_string()), "bar".to_string())
413        );
414    }
415
416    fn call_node(tree: &Tree) -> Node {
417        let root_node = tree.root_node();
418        root_node.named_child(1).unwrap()
419    }
420}