1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//! `Checker` implementation for PHP.
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use super::*;
impl Checker for PhpCode {
fn is_comment(node: &Node) -> bool {
node.kind_id() == Php::Comment
}
fn is_func_space(node: &Node) -> bool {
matches!(
node.kind_id().into(),
Php::Program
| Php::FunctionDefinition
| Php::MethodDeclaration
| Php::AnonymousFunction
| Php::ArrowFunction
| Php::ClassDeclaration
| Php::InterfaceDeclaration
| Php::TraitDeclaration
| Php::EnumDeclaration
| Php::AnonymousClass
)
}
fn is_func(node: &Node) -> bool {
matches!(
node.kind_id().into(),
Php::FunctionDefinition | Php::MethodDeclaration
)
}
fn is_closure(node: &Node) -> bool {
matches!(
node.kind_id().into(),
Php::AnonymousFunction | Php::ArrowFunction
)
}
// Intentionally narrower than ABC's `branches` set: ABC additionally
// counts `ObjectCreationExpression` (`new Foo()`) as a branch, but
// `is_call` drives the `--ops` CLI feature and should match the
// user's mental model of "function/method call sites" (mirrors
// Java's `is_call` = `MethodInvocation` while ABC counts
// `MethodInvocation | New`).
fn is_call(node: &Node) -> bool {
matches!(
node.kind_id().into(),
Php::FunctionCallExpression
| Php::MemberCallExpression
| Php::ScopedCallExpression
| Php::NullsafeMemberCallExpression
)
}
fn is_non_arg(node: &Node) -> bool {
matches!(
node.kind_id().into(),
Php::LPAREN | Php::LPAREN2 | Php::COMMA | Php::RPAREN | Php::RPAREN2 | Php::DOTDOTDOT
)
}
// `String` is the named single-quoted literal; `String2` and
// `String3` are aliased kind_ids that the language enum also
// maps to `"string"` (`String2` is the `string` type keyword
// and `String3` is the hidden `_string` supertype that covers
// any string literal). Include all three so generic
// string-filtering stays consistent with `get_op_type` and the
// `Alterator` text-preservation arm (issue #288).
impl_simple_is_string!(
Php,
String,
String2,
String3,
EncapsedString,
Heredoc,
Nowdoc,
ShellCommandExpression,
);
// PHP models `else if` in two distinct shapes, so it cannot use a
// single is_else_if macro:
//
// 1. The one-word `elseif` keyword parses as a dedicated
// `else_if_clause` node (`ElseIfClause`); both the brace
// `} elseif {` and the alternative `elseif: … endif;` colon forms
// parse as `ElseIfClause`. `ElseIfClause2` is an aliased kind_id
// the grammar maps to the same `else_if_clause` rule name but which
// does not surface in observed parse trees; it is kept as a
// defensive arm (lesson #34) so a future grammar revision that emits
// it is handled rather than silently dropped.
// 2. The two-word `else if` form (only valid in the brace syntax)
// parses as an `else_clause` wrapping a nested `if_statement`
// (`else_clause → if_statement`, the `impl_is_else_if_parent_clause!`
// shape used by C++/JS/Rust). The inner `IfStatement` must be
// recognized as an else-if continuation, or the cognitive
// `IfStatement` arm double-counts it and inflates nesting for later
// arms (#529). A plain `else { if (…) {} }` does NOT match: there the
// inner `if` nests under a `compound_statement`, not directly under
// the `else_clause`.
//
// The alternative colon syntax (`if …: … endif;`) only accepts the
// one-word `elseif` (shape 1); two-word `else if` there is a PHP fatal
// parse error and the grammar emits an `ERROR` node, so it is
// intentionally not handled here — there is no well-defined metric for
// syntactically invalid source.
//
// Matching shapes 1 and 2 lets the cognitive guard
// `IfStatement if !Self::is_else_if(node)` suppress the nested-if
// penalty while the wrapping `else_clause` still scores its +1
// branch extension. The dedicated-clause match (shape 1) is
// behaviorally inert today — `count_specific_ancestors` is not used by
// PHP cognitive and the guard only fires on `IfStatement` — but is kept
// for parity with the PHP cyclomatic/ABC/cognitive dispatch which list
// both clause variants.
#[inline]
fn is_else_if(node: &Node) -> bool {
let kind = node.kind_id();
matches!(kind.into(), Php::ElseIfClause | Php::ElseIfClause2)
|| (kind == Php::IfStatement
&& node.parent().is_some_and(|parent| {
matches!(parent.kind_id().into(), Php::ElseClause | Php::ElseClause2)
}))
}
}