Skip to main content

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}