big_code_analysis_ast/getter/php.rs
1//! `Getter` implementation for PHP.
2#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
3
4use super::*;
5
6impl Getter for PhpCode {
7 fn get_space_kind(node: &Node) -> SpaceKind {
8 match node.kind_id().into() {
9 // PHP traits are class-like mixins whose method
10 // implementations roll up into the consuming class's WMC; we
11 // map them to `SpaceKind::Class` so the per-class metrics
12 // (NPA, NPM, WMC) treat them uniformly. The output may label
13 // them "class" — that is intentional for metric coherence.
14 // LOAD-BEARING: `Wmc::compute` for PhpCode does not match
15 // `SpaceKind::Trait`. If you remap `TraitDeclaration` here,
16 // also update `src/metrics/wmc.rs`.
17 Php::ClassDeclaration
18 | Php::AnonymousClass
19 | Php::EnumDeclaration
20 | Php::TraitDeclaration => SpaceKind::Class,
21 Php::InterfaceDeclaration => SpaceKind::Interface,
22 Php::FunctionDefinition
23 | Php::MethodDeclaration
24 | Php::AnonymousFunction
25 | Php::ArrowFunction => SpaceKind::Function,
26 Php::Program => SpaceKind::Unit,
27 _ => SpaceKind::Unknown,
28 }
29 }
30
31 fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
32 use Php::*;
33 match node.kind_id().into() {
34 // String-interpolation opener. `LBRACE` is *both* the
35 // compound-statement brace and the complex-interpolation
36 // opener, so `"dq {$y} end"` reported a `{}` operator —
37 // and reported it against the same vocabulary entry a real
38 // block uses, which no other language does (#1314).
39 //
40 // An interpolation opener is spelling, not an operation:
41 // the interpolated expression's own operators are already
42 // counted, and no reader performs a `{`. Measured across
43 // the six interpolating languages here, `[n1, N1]` on one
44 // fixture with and without an interpolation: kotlin
45 // [5,7]->[5,7], csharp [8,11]->[8,11], groovy's `${` (142)
46 // absent from its operator arm, ruby [1,2]->[2,3], php
47 // [2,4]->[3,5], and elixir counting it through the same
48 // `HASHLBRACE` token as Ruby. Three of six already
49 // declined to count it; this arm plus the Ruby and Elixir
50 // `#{` drops on this branch make it six of six.
51 //
52 // Four parents, not one. The opener is a direct child of
53 // `encapsed_string` (`"{$y}"`), of `heredoc_body` — the
54 // body, not `heredoc` — of `shell_command_expression`
55 // (`` `ls {$y}` ``), and of `dynamic_variable_name`, which
56 // covers both the in-string `"${y}"` form and a bare
57 // `${$y}` variable-variable. `"${y}"` is deprecated as of
58 // PHP 8.2 and removed in 9.0, but the grammar still parses
59 // it and it is still in the wild.
60 //
61 // Parent, not ancestor, and PHP is one of only two
62 // languages in #1314 where that is observable: a closure
63 // inside an interpolation (`"{$o->m(function() { … })}"`)
64 // puts a compound-statement brace under an
65 // `encapsed_string` ancestor. Pinned by
66 // `php_interpolation_guard_is_parent_scoped_not_ancestor_scoped`.
67 //
68 // A literal brace in string text (`"lit { brace"`) never
69 // reaches here — the grammar folds it into `string_content`
70 // — so nothing is over-suppressed.
71 LBRACE
72 if matches!(
73 ancestors.parent(node).map(|p| p.kind_id().into()),
74 Some(
75 EncapsedString
76 | HeredocBody
77 | ShellCommandExpression
78 | DynamicVariableName
79 )
80 ) =>
81 {
82 TokenRole::Unknown
83 }
84 // Operator: control-flow keywords
85 If | Else | Elseif | Endif
86 | Switch | Case | Default | Endswitch
87 | For | Endfor | Foreach | Endforeach
88 | While | Endwhile | Do
89 | Break | Continue
90 | Return | Throw | Try | Catch | Finally
91 | Match | Yield | Yieldfrom | Goto
92 | Echo | Exit | Print
93 | Include | IncludeOnce | Require | RequireOnce
94
95 // Operator: declaration keywords
96 | Function | Class | Interface | Trait | Enum | Namespace
97 | Use | Const | Global | Static | VarModifier
98 | Public | Protected | Private
99 | Final | Abstract | Readonly
100 | New | Clone | Instanceof | As | Insteadof | Extends | Implements
101 | Fn | Declare | Enddeclare | Unset | List
102 | Zelf | Parent
103
104 // Operator: structural punctuation. Only the *opening*
105 // delimiter is an operator (the pair folds to one glyph in
106 // `get_operator_id_as_str`); the former closing arms
107 // (`RBRACE`/`RPAREN`/`RPAREN2`/`RBRACK`) double-counted every
108 // balanced pair, inflating n1/N1 (#695). `LPAREN2` is defensive —
109 // the runtime collapses it to `LPAREN` before `kind_id()`, so it
110 // never fires (#768; see the Cpp note).
111 | LBRACE | LPAREN | LPAREN2
112 | LBRACK
113 | COMMA | SEMI | COLON | COLONCOLON
114 | DASHGT | QMARKDASHGT | EQGT | BSLASH | DOTDOTDOT | QMARK | AT
115 | HASHLBRACK
116
117 // Operator: arithmetic
118 | PLUS | DASH | STAR | SLASH | PERCENT | STARSTAR
119 | PLUSPLUS | DASHDASH
120
121 // Operator: comparison
122 | EQEQ | EQEQEQ | BANGEQ | BANGEQEQ | LTGT
123 | LT | GT | LTEQ | GTEQ | LTEQGT
124
125 // Operator: logical
126 | AMPAMP | PIPEPIPE | BANG
127 | And | Or | Xor | QMARKQMARK
128
129 // Operator: bitwise
130 | AMP | PIPE | CARET | TILDE | LTLT | GTGT
131
132 // Operator: assignment
133 | EQ
134 | PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ | STARSTAREQ
135 | DOTEQ | QMARKQMARKEQ
136 | AMPEQ | PIPEEQ | CARETEQ | LTLTEQ | GTGTEQ
137
138 // Operator: string concat
139 | DOT
140 => TokenRole::Operator,
141
142 // `name` is a genuine operand almost everywhere — function,
143 // class, const, member (`->prop`) and namespace-component
144 // identifiers are all this kind. But it is ALSO the identifier
145 // leaf of every variable reference: `$x` parses as a
146 // `variable_name` wrapping `name`, and `"${x}"` as a
147 // `dynamic_variable_name` wrapping it directly. Both wrappers
148 // are operands in their own right below (keyed with the `$`
149 // sigil, the way Bash and iRules key theirs), so counting the
150 // leaf too double-counts every variable reference — inflating
151 // N2 and adding a spurious sigil-less twin to n2 (#1259).
152 // Exclude it exactly in those two positions, the same
153 // parent-scoped guard Bash applies to `variable_name` under
154 // `simple_expansion` and iRules to `Id` under
155 // `variable_substitution`. `Name2` is the hidden `_name`
156 // supertype the parser never emits — pinned by
157 // `php_name2_hidden_rule_drift_marker` in `src/metrics/abc.rs`.
158 // It rides along defensively so a grammar bump that promotes
159 // it inherits the guard rather than the bug.
160 Name | Name2 => match ancestors.parent(node).map(|p| p.kind_id().into()) {
161 Some(VariableName | DynamicVariableName) => TokenRole::Unknown,
162 _ => TokenRole::Operand,
163 },
164
165 // Variable-variable syntax nests these wrappers: `$$x` is a
166 // `dynamic_variable_name` around a `variable_name`, `${$x}`
167 // the same, and `$$$x` a `dynamic_variable_name` around
168 // another `dynamic_variable_name`. Each level's text span
169 // contains the one below it, so only the outermost may count;
170 // suppressing both kinds under a `dynamic_variable_name`
171 // parent handles arbitrary depth (#1259). An inner reference
172 // reached through a real expression (`${$x . 'y'}`, whose
173 // `$x` hangs off a `binary_expression`) is unaffected.
174 VariableName | DynamicVariableName => {
175 match ancestors.parent(node).map(|p| p.kind_id().into()) {
176 Some(DynamicVariableName) => TokenRole::Unknown,
177 _ => TokenRole::Operand,
178 }
179 }
180
181 // `named_type`, `optional_type`, `union_type`,
182 // `intersection_type`, `disjunctive_normal_form_type`,
183 // `qualified_name`, `relative_name` and `namespace_name`
184 // are deliberately absent from the operand arm below and
185 // fall through to the `_ => Unknown` catch-all (#1293).
186 // They cannot be spelled as an explicit `Unknown` arm —
187 // `clippy::match_same_arms` rejects one that duplicates the
188 // catch-all — so the rationale lives here.
189 //
190 // Each of them spans exactly the text of the node(s) it
191 // contains plus separator glyphs that `get_op_type` already
192 // counts as *operators* — `?` (`QMARK`), `|` (`PIPE`), `&`
193 // (`AMP`), the DNF parens, and `\` (`BSLASH`). Counting the
194 // wrapper as well billed the same bytes two to five times:
195 // `?A\B` nests `optional_type` → `named_type` →
196 // `qualified_name` → `namespace_name` → `name` and scored 6
197 // operands for two identifiers.
198 //
199 // Both design calls this needed, recorded so the next
200 // reader does not have to re-derive them:
201 //
202 // * A type's operand is the innermost concrete type, not
203 // the decorated form: `?int` is the operand `int`, not
204 // `?int`. The `?` is already an operator, so folding it
205 // into the operand text bills nullability across both
206 // Halstead halves and splits `int` and `?int` into two
207 // vocabulary entries for one type.
208 // * A qualified name's operands are its components, not
209 // the whole path: `Foo\Bar\Baz` is `Foo`, `Bar`, `Baz`
210 // with two `\` operators. That is exactly how PHP's own
211 // sibling separators already read here — `Foo::bar`
212 // yields operands `Foo` / `bar` around a `::` operator,
213 // `$o->prop` yields `$o` / `prop` around `->` — and how
214 // Rust's `foo::bar::baz` reads. The whole-path reading
215 // would instead plant a distinct vocabulary entry per
216 // prefix (`Foo`, `Foo\Bar`, `Foo\Bar\Baz`).
217 //
218 // Dropping these kinds rather than gating a leaf is safe
219 // here, unlike `primitive_type` below: every one of them is
220 // grammatically required to contain the node that now
221 // carries the operand (a `named_type` always wraps a `name`
222 // or `qualified_name`, a `namespace_name` always holds at
223 // least one `name`), so no spelling regresses to zero the
224 // way grammar-dispatch §6 warns about.
225
226 // The primitive-type keyword tokens, suppressed under a
227 // `primitive_type` wrapper (#1293). Here the wrapper is the
228 // node that carries the operand and the leaf is the
229 // duplicate — the opposite of the qualified-name arm above,
230 // and deliberately so: `primitive_type` is *childless* for
231 // `callable`, `iterable`, `mixed`, `void`, `false` and
232 // `true` (verified with `bca dump`; the grammar emits no
233 // token node for those spellings), so dropping the wrapper
234 // would score those six types zero — grammar-dispatch §6
235 // exactly. The wrapper's text equals the keyword's, so
236 // `n2` is unchanged and only the duplicate `N2` occurrence
237 // goes away.
238 //
239 // Parent-scoped, not a blanket exclusion: `array` is also
240 // the head token of `array(1, 2)` and must keep counting
241 // there, and a `(string)` cast is a childless `cast_type`
242 // that never reaches this arm.
243 //
244 // Cross-walk (grammar-dispatch §7): `Checker::is_string`
245 // agrees — #1474 withdrew `String2` from it on the strength
246 // of this arm, so a `: string` return type is neither a
247 // Halstead operand here nor a literal to `find string`.
248 // The *span* keeps its operand either way: the enclosing
249 // `primitive_type` carries it, and only the node it is
250 // attributed to moved.
251 Int | Bool | Array | Object | String2 | Float2 | Null2 => {
252 match ancestors.parent(node).map(|p| p.kind_id().into()) {
253 Some(PrimitiveType) => TokenRole::Unknown,
254 _ => TokenRole::Operand,
255 }
256 }
257
258 // Operands: literals and the type nodes that are leaves or
259 // carry the operand themselves. `String` (368) is the
260 // single-quoted string literal and `String3` the hidden
261 // `_string` supertype — the `string` *type* keyword is
262 // `String2` (25), handled above. Neither `String` nor
263 // `Nowdoc` ever interpolates, so both are always one
264 // operand. `bottom_type` (`never`) and `cast_type`
265 // (`(int)`) are leaves with no inner node to double-count.
266 Integer | Float
267 | String | String3
268 | Nowdoc
269 | Boolean | Null
270 | BottomType
271 | PrimitiveType | CastType
272 => TokenRole::Operand,
273
274 // `EncapsedString` (double-quoted), `Heredoc`, and
275 // `ShellCommandExpression` (backticks) count as one
276 // operand when inert. When they carry a `$var`,
277 // `${name}`, or `{$expr}` interpolation child, those
278 // inner expressions are already walked and classified as
279 // operands in their own right; counting the wrapping
280 // literal too would double-count their contribution to
281 // `N2` (issue #184, same pattern as #180 for Elixir/Bash
282 // and #183 for C#). `ShellCommandExpression` was previously
283 // omitted entirely (issue #288), so backtick literals
284 // contributed no Halstead operand at all even when inert.
285 EncapsedString | Heredoc | ShellCommandExpression => {
286 // PHP's interpolation children appear directly on the
287 // wrapping literal, except `Heredoc`, which holds them
288 // one level down under a single `heredoc_body` child —
289 // so the descend below mirrors the original
290 // `php_string_has_interpolation` two-level walk.
291 const PHP_INTERP_KINDS: &[u16] = &[
292 // `"$name"` → direct `variable_name` child.
293 VariableName as u16,
294 // `"${name}"` → direct `dynamic_variable_name` child.
295 DynamicVariableName as u16,
296 // `"$arr[0]"` → direct `subscript_expression` child.
297 // The grammar gives this kind three numeric aliases.
298 SubscriptExpression as u16,
299 SubscriptExpression2 as u16,
300 SubscriptExpression3 as u16,
301 // `"$obj->prop"` → direct `member_access_expression`
302 // child. PHP's bare-interpolation syntax does not
303 // support `?->` (nullsafe) or `::` (scope), so only
304 // member-access aliases need handling here; nullsafe /
305 // scope forms always go through the `{ … }` wrapper.
306 MemberAccessExpression as u16,
307 MemberAccessExpression2 as u16,
308 MemberAccessExpression3 as u16,
309 // `"{$expr}"` → anonymous `{` (LBRACE) opens the
310 // complex-interpolation wrapper whose body is an
311 // arbitrary expression; the brace appears as a direct
312 // child.
313 LBRACE as u16,
314 ];
315 // Single pass over the direct children: an interpolation
316 // child on the literal itself (EncapsedString /
317 // ShellCommandExpression) OR, for Heredoc, one nested under
318 // its `heredoc_body` child. Folding both checks into one walk
319 // avoids re-scanning the children a second time through
320 // `string_operand_type` for an inert heredoc.
321 let has_interp = node.children().any(|c| {
322 let kind = c.kind_id();
323 PHP_INTERP_KINDS.contains(&kind)
324 || (kind == HeredocBody as u16 && c.wraps_any(PHP_INTERP_KINDS))
325 });
326 if has_interp {
327 TokenRole::Unknown
328 } else {
329 TokenRole::Operand
330 }
331 }
332
333 _ => TokenRole::Unknown,
334 }
335 }
336
337 get_operator!(Php);
338}