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