• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sirbrillig / phpcs-variable-analysis / 12123773285

02 Dec 2024 04:34PM UTC coverage: 93.812% (-0.07%) from 93.878%
12123773285

Pull #342

github

sirbrillig
Guard for no enclosing scope
Pull Request #342: Only search for nested arrow functions if necessary

16 of 16 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

1880 of 2004 relevant lines covered (93.81%)

136.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.36
/VariableAnalysis/Lib/Helpers.php
1
<?php
2

3
namespace VariableAnalysis\Lib;
4

5
use PHP_CodeSniffer\Files\File;
6
use VariableAnalysis\Lib\ScopeInfo;
7
use VariableAnalysis\Lib\Constants;
8
use VariableAnalysis\Lib\ForLoopInfo;
9
use VariableAnalysis\Lib\EnumInfo;
10
use VariableAnalysis\Lib\ScopeType;
11
use VariableAnalysis\Lib\VariableInfo;
12
use PHP_CodeSniffer\Util\Tokens;
13

14
class Helpers
15
{
16
        /**
17
         * @return array<int|string>
18
         */
19
        public static function getPossibleEndOfFileTokens()
352✔
20
        {
21
                return array_merge(
352✔
22
                        array_values(Tokens::$emptyTokens),
352✔
23
                        [
176✔
24
                                T_INLINE_HTML,
352✔
25
                                T_CLOSE_TAG,
352✔
26
                        ]
176✔
27
                );
352✔
28
        }
29

30
        /**
31
         * @param int|bool $value
32
         *
33
         * @return ?int
34
         */
35
        public static function getIntOrNull($value)
348✔
36
        {
37
                return is_int($value) ? $value : null;
348✔
38
        }
39

40
        /**
41
         * Find the position of the square bracket containing the token at $stackPtr,
42
         * if any.
43
         *
44
         * @param File $phpcsFile
45
         * @param int  $stackPtr
46
         *
47
         * @return ?int
48
         */
49
        public static function findContainingOpeningSquareBracket(File $phpcsFile, $stackPtr)
332✔
50
        {
51
                // Find the previous bracket within this same statement.
52
                $previousStatementPtr = self::getPreviousStatementPtr($phpcsFile, $stackPtr);
332✔
53
                $openBracketPosition = self::getIntOrNull($phpcsFile->findPrevious([T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET], $stackPtr - 1, $previousStatementPtr));
332✔
54
                if (empty($openBracketPosition)) {
332✔
55
                        return null;
332✔
56
                }
57
                // Make sure we are inside the pair of brackets we found.
58
                $tokens = $phpcsFile->getTokens();
68✔
59
                $openBracketToken = $tokens[$openBracketPosition];
68✔
60
                if (empty($openBracketToken) || empty($tokens[$openBracketToken['bracket_closer']])) {
68✔
61
                        return null;
×
62
                }
63
                $closeBracketPosition = $openBracketToken['bracket_closer'];
68✔
64
                if (empty($closeBracketPosition)) {
68✔
65
                        return null;
×
66
                }
67
                if ($stackPtr > $closeBracketPosition) {
68✔
68
                        return null;
68✔
69
                }
70
                return $openBracketPosition;
48✔
71
        }
72

73
        /**
74
         * @param File $phpcsFile
75
         * @param int  $stackPtr
76
         *
77
         * @return int
78
         */
79
        public static function getPreviousStatementPtr(File $phpcsFile, $stackPtr)
332✔
80
        {
81
                $result = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET], $stackPtr - 1);
332✔
82
                return is_bool($result) ? 1 : $result;
332✔
83
        }
84

85
        /**
86
         * @param File $phpcsFile
87
         * @param int  $stackPtr
88
         *
89
         * @return ?int
90
         */
91
        public static function findContainingOpeningBracket(File $phpcsFile, $stackPtr)
348✔
92
        {
93
                $tokens = $phpcsFile->getTokens();
348✔
94
                if (isset($tokens[$stackPtr]['nested_parenthesis'])) {
348✔
95
                        /**
96
                         * @var array<int|string|null>
97
                         */
98
                        $openPtrs = array_keys($tokens[$stackPtr]['nested_parenthesis']);
256✔
99
                        return (int)end($openPtrs);
256✔
100
                }
101
                return null;
344✔
102
        }
103

104
        /**
105
         * @param array{conditions: (int|string)[], content: string} $token
106
         *
107
         * @return bool
108
         */
109
        public static function areAnyConditionsAClass(array $token)
40✔
110
        {
111
                $conditions = $token['conditions'];
40✔
112
                $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
40✔
113
                if (defined('T_ENUM')) {
40✔
114
                        $classlikeCodes[] = T_ENUM;
20✔
115
                }
10✔
116
                $classlikeCodes[] = 'PHPCS_T_ENUM';
40✔
117
                foreach (array_reverse($conditions, true) as $scopeCode) {
40✔
118
                        if (in_array($scopeCode, $classlikeCodes, true)) {
40✔
119
                                return true;
28✔
120
                        }
121
                }
20✔
122
                return false;
20✔
123
        }
124

125
        /**
126
         * Return true if the token conditions are within a function before they are
127
         * within a class.
128
         *
129
         * @param array{conditions: (int|string)[], content: string} $token
130
         *
131
         * @return bool
132
         */
133
        public static function areConditionsWithinFunctionBeforeClass(array $token)
324✔
134
        {
135
                $conditions = $token['conditions'];
324✔
136
                $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
324✔
137
                if (defined('T_ENUM')) {
324✔
138
                        $classlikeCodes[] = T_ENUM;
162✔
139
                }
81✔
140
                $classlikeCodes[] = 'PHPCS_T_ENUM';
324✔
141
                foreach (array_reverse($conditions, true) as $scopeCode) {
324✔
142
                        if (in_array($scopeCode, $classlikeCodes)) {
324✔
143
                                return false;
4✔
144
                        }
145
                        if ($scopeCode === T_FUNCTION) {
324✔
146
                                return true;
320✔
147
                        }
148
                }
62✔
149
                return false;
12✔
150
        }
151

152
        /**
153
         * Return true if the token conditions are within an IF/ELSE/ELSEIF block
154
         * before they are within a class or function.
155
         *
156
         * @param (int|string)[] $conditions
157
         *
158
         * @return int|string|null
159
         */
160
        public static function getClosestConditionPositionIfBeforeOtherConditions(array $conditions)
8✔
161
        {
162
                $conditionsInsideOut = array_reverse($conditions, true);
8✔
163
                if (empty($conditions)) {
8✔
164
                        return null;
×
165
                }
166
                $scopeCode = reset($conditionsInsideOut);
8✔
167
                $conditionalCodes = [
4✔
168
                        T_IF,
8✔
169
                        T_ELSE,
8✔
170
                        T_ELSEIF,
8✔
171
                ];
8✔
172
                if (in_array($scopeCode, $conditionalCodes, true)) {
8✔
173
                        return key($conditionsInsideOut);
8✔
174
                }
175
                return null;
8✔
176
        }
177

178
        /**
179
         * @param File $phpcsFile
180
         * @param int  $stackPtr
181
         *
182
         * @return bool
183
         */
184
        public static function isTokenFunctionParameter(File $phpcsFile, $stackPtr)
352✔
185
        {
186
                return is_int(self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr));
352✔
187
        }
188

189
        /**
190
         * Return true if the token is inside the arguments of a function call.
191
         *
192
         * For example, the variable `$foo` in `doSomething($foo)` is inside the
193
         * arguments to the call to `doSomething()`.
194
         *
195
         * @param File $phpcsFile
196
         * @param int  $stackPtr
197
         *
198
         * @return bool
199
         */
200
        public static function isTokenInsideFunctionCallArgument(File $phpcsFile, $stackPtr)
320✔
201
        {
202
                return is_int(self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr));
320✔
203
        }
204

205
        /**
206
         * Find the index of the function keyword for a token in a function
207
         * definition's parameters.
208
         *
209
         * Does not work for tokens inside the "use".
210
         *
211
         * Will also work for the parenthesis that make up the function definition's
212
         * parameters list.
213
         *
214
         * For arguments inside a function call, rather than a definition, use
215
         * `getFunctionIndexForFunctionCallArgument`.
216
         *
217
         * @param File $phpcsFile
218
         * @param int  $stackPtr
219
         *
220
         * @return ?int
221
         */
222
        public static function getFunctionIndexForFunctionParameter(File $phpcsFile, $stackPtr)
352✔
223
        {
224
                $tokens = $phpcsFile->getTokens();
352✔
225
                $token = $tokens[$stackPtr];
352✔
226
                if ($token['code'] === 'PHPCS_T_OPEN_PARENTHESIS') {
352✔
227
                        $startOfArguments = $stackPtr;
×
228
                } elseif ($token['code'] === 'PHPCS_T_CLOSE_PARENTHESIS') {
352✔
229
                        if (empty($token['parenthesis_opener'])) {
24✔
230
                                return null;
×
231
                        }
232
                        $startOfArguments = $token['parenthesis_opener'];
24✔
233
                } else {
12✔
234
                        if (empty($token['nested_parenthesis'])) {
352✔
235
                                return null;
348✔
236
                        }
237
                        $startingParenthesis = array_keys($token['nested_parenthesis']);
280✔
238
                        $startOfArguments = end($startingParenthesis);
280✔
239
                }
240

241
                if (! is_int($startOfArguments)) {
280✔
242
                        return null;
×
243
                }
244

245
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
280✔
246
                $nonFunctionTokenTypes[] = T_STRING;
280✔
247
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
280✔
248
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
280✔
249
                if (! is_int($functionPtr)) {
280✔
250
                        return null;
×
251
                }
252
                $functionToken = $tokens[$functionPtr];
280✔
253

254
                $functionTokenTypes = [
140✔
255
                        T_FUNCTION,
280✔
256
                        T_CLOSURE,
280✔
257
                ];
280✔
258
                if (!in_array($functionToken['code'], $functionTokenTypes, true) && ! self::isArrowFunction($phpcsFile, $functionPtr)) {
280✔
259
                        return null;
260✔
260
                }
261
                return $functionPtr;
232✔
262
        }
263

264
        /**
265
         * @param File $phpcsFile
266
         * @param int  $stackPtr
267
         *
268
         * @return bool
269
         */
270
        public static function isTokenInsideFunctionUseImport(File $phpcsFile, $stackPtr)
348✔
271
        {
272
                return is_int(self::getUseIndexForUseImport($phpcsFile, $stackPtr));
348✔
273
        }
274

275
        /**
276
         * Find the token index of the "use" for a token inside a function use import
277
         *
278
         * @param File $phpcsFile
279
         * @param int  $stackPtr
280
         *
281
         * @return ?int
282
         */
283
        public static function getUseIndexForUseImport(File $phpcsFile, $stackPtr)
348✔
284
        {
285
                $tokens = $phpcsFile->getTokens();
348✔
286

287
                $nonUseTokenTypes = Tokens::$emptyTokens;
348✔
288
                $nonUseTokenTypes[] = T_VARIABLE;
348✔
289
                $nonUseTokenTypes[] = T_ELLIPSIS;
348✔
290
                $nonUseTokenTypes[] = T_COMMA;
348✔
291
                $nonUseTokenTypes[] = T_BITWISE_AND;
348✔
292
                $openParenPtr = self::getIntOrNull($phpcsFile->findPrevious($nonUseTokenTypes, $stackPtr - 1, null, true, null, true));
348✔
293
                if (! is_int($openParenPtr) || $tokens[$openParenPtr]['code'] !== T_OPEN_PARENTHESIS) {
348✔
294
                        return null;
344✔
295
                }
296

297
                $usePtr = self::getIntOrNull($phpcsFile->findPrevious(array_values($nonUseTokenTypes), $openParenPtr - 1, null, true, null, true));
220✔
298
                if (! is_int($usePtr) || $tokens[$usePtr]['code'] !== T_USE) {
220✔
299
                        return null;
208✔
300
                }
301
                return $usePtr;
24✔
302
        }
303

304
        /**
305
         * Return the index of a function's name token from inside the function.
306
         *
307
         * $stackPtr must be inside the function body or parameters for this to work.
308
         *
309
         * @param File $phpcsFile
310
         * @param int  $stackPtr
311
         *
312
         * @return ?int
313
         */
314
        public static function findFunctionCall(File $phpcsFile, $stackPtr)
328✔
315
        {
316
                $tokens = $phpcsFile->getTokens();
328✔
317

318
                $openPtr = self::findContainingOpeningBracket($phpcsFile, $stackPtr);
328✔
319
                if (is_int($openPtr)) {
328✔
320
                        // First non-whitespace thing and see if it's a T_STRING function name
321
                        $functionPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
212✔
322
                        if (is_int($functionPtr) && $tokens[$functionPtr]['code'] === T_STRING) {
212✔
323
                                return $functionPtr;
158✔
324
                        }
325
                }
71✔
326
                return null;
314✔
327
        }
328

329
        /**
330
         * @param File $phpcsFile
331
         * @param int  $stackPtr
332
         *
333
         * @return array<int, array<int>>
334
         */
335
        public static function findFunctionCallArguments(File $phpcsFile, $stackPtr)
32✔
336
        {
337
                $tokens = $phpcsFile->getTokens();
32✔
338

339
                // Slight hack: also allow this to find args for array constructor.
340
                if (($tokens[$stackPtr]['code'] !== T_STRING) && ($tokens[$stackPtr]['code'] !== T_ARRAY)) {
32✔
341
                        // Assume $stackPtr is something within the brackets, find our function call
342
                        $stackPtr = self::findFunctionCall($phpcsFile, $stackPtr);
20✔
343
                        if ($stackPtr === null) {
20✔
344
                                return [];
×
345
                        }
346
                }
10✔
347

348
                // $stackPtr is the function name, find our brackets after it
349
                $openPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
32✔
350
                if (($openPtr === false) || ($tokens[$openPtr]['code'] !== T_OPEN_PARENTHESIS)) {
32✔
351
                        return [];
×
352
                }
353

354
                if (!isset($tokens[$openPtr]['parenthesis_closer'])) {
32✔
355
                        return [];
×
356
                }
357
                $closePtr = $tokens[$openPtr]['parenthesis_closer'];
32✔
358

359
                $argPtrs = [];
32✔
360
                $lastPtr = $openPtr;
32✔
361
                $lastArgComma = $openPtr;
32✔
362
                $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
32✔
363
                while (is_int($nextPtr)) {
32✔
364
                        if (self::findContainingOpeningBracket($phpcsFile, $nextPtr) === $openPtr) {
32✔
365
                                // Comma is at our level of brackets, it's an argument delimiter.
366
                                $range = range($lastArgComma + 1, $nextPtr - 1);
32✔
367
                                $range = array_filter($range, function ($element) {
16✔
368
                                        return is_int($element);
32✔
369
                                });
32✔
370
                                array_push($argPtrs, $range);
32✔
371
                                $lastArgComma = $nextPtr;
32✔
372
                        }
16✔
373
                        $lastPtr = $nextPtr;
32✔
374
                        $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
32✔
375
                }
16✔
376
                $range = range($lastArgComma + 1, $closePtr - 1);
32✔
377
                $range = array_filter($range, function ($element) {
16✔
378
                        return is_int($element);
32✔
379
                });
32✔
380
                array_push($argPtrs, $range);
32✔
381

382
                return $argPtrs;
32✔
383
        }
384

385
        /**
386
         * @param File $phpcsFile
387
         * @param int  $stackPtr
388
         *
389
         * @return ?int
390
         */
391
        public static function getNextAssignPointer(File $phpcsFile, $stackPtr)
340✔
392
        {
393
                $tokens = $phpcsFile->getTokens();
340✔
394

395
                // Is the next non-whitespace an assignment?
396
                $nextPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
340✔
397
                if (is_int($nextPtr)
340✔
398
                        && isset(Tokens::$assignmentTokens[$tokens[$nextPtr]['code']])
340✔
399
                        // Ignore double arrow to prevent triggering on `foreach ( $array as $k => $v )`.
400
                        && $tokens[$nextPtr]['code'] !== T_DOUBLE_ARROW
340✔
401
                ) {
170✔
402
                        return $nextPtr;
320✔
403
                }
404
                return null;
332✔
405
        }
406

407
        /**
408
         * @param string $varName
409
         *
410
         * @return string
411
         */
412
        public static function normalizeVarName($varName)
352✔
413
        {
414
                $result = preg_replace('/[{}$]/', '', $varName);
352✔
415
                return $result ? $result : $varName;
352✔
416
        }
417

418
        /**
419
         * @param File   $phpcsFile
420
         * @param int    $stackPtr
421
         * @param string $varName   (optional) if it differs from the normalized 'content' of the token at $stackPtr
422
         *
423
         * @return ?int
424
         */
425
        public static function findVariableScope(File $phpcsFile, $stackPtr, $varName = null)
352✔
426
        {
427
                $tokens = $phpcsFile->getTokens();
352✔
428
                $token = $tokens[$stackPtr];
352✔
429
                $varName = isset($varName) ? $varName : self::normalizeVarName($token['content']);
352✔
430

431
                $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
352✔
432
                if ($enclosingScopeIndex) {
352✔
433
                        $arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
332✔
434
                        $isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex);
332✔
435
                        if ($isTokenInsideArrowFunctionBody) {
332✔
436
                                // Get the list of variables defined by the arrow function
437
                                // If this matches any of them, the scope is the arrow function,
438
                                // otherwise, it uses the enclosing scope.
439
                                if ($arrowFunctionIndex) {
36✔
440
                                        $variableNames = self::getVariablesDefinedByArrowFunction($phpcsFile, $arrowFunctionIndex);
36✔
441
                                        self::debug('findVariableScope: looking for', $varName, 'in arrow function variables', $variableNames);
36✔
442
                                        if (in_array($varName, $variableNames, true)) {
36✔
443
                                                return $arrowFunctionIndex;
36✔
444
                                        }
445
                                }
14✔
446
                        }
14✔
447
                }
166✔
448

449
                return self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
352✔
450
        }
451

452
        /**
453
         * Return the variable names and positions of each variable targetted by a `compact()` call.
454
         *
455
         * @param File                   $phpcsFile
456
         * @param int                    $stackPtr
457
         * @param array<int, array<int>> $arguments The stack pointers of each argument; see findFunctionCallArguments
458
         *
459
         * @return array<VariableInfo> each variable's firstRead position and its name; other VariableInfo properties are not set!
460
         */
461
        public static function getVariablesInsideCompact(File $phpcsFile, $stackPtr, $arguments)
12✔
462
        {
463
                $tokens = $phpcsFile->getTokens();
12✔
464
                $variablePositionsAndNames = [];
12✔
465

466
                foreach ($arguments as $argumentPtrs) {
12✔
467
                        $argumentPtrs = array_values(array_filter($argumentPtrs, function ($argumentPtr) use ($tokens) {
12✔
468
                                return isset(Tokens::$emptyTokens[$tokens[$argumentPtr]['code']]) === false;
12✔
469
                        }));
12✔
470
                        if (empty($argumentPtrs)) {
12✔
471
                                continue;
×
472
                        }
473
                        if (!isset($tokens[$argumentPtrs[0]])) {
12✔
474
                                continue;
×
475
                        }
476
                        $argumentFirstToken = $tokens[$argumentPtrs[0]];
12✔
477
                        if ($argumentFirstToken['code'] === T_ARRAY) {
12✔
478
                                // It's an array argument, recurse.
479
                                $arrayArguments = self::findFunctionCallArguments($phpcsFile, $argumentPtrs[0]);
12✔
480
                                $variablePositionsAndNames = array_merge($variablePositionsAndNames, self::getVariablesInsideCompact($phpcsFile, $stackPtr, $arrayArguments));
12✔
481
                                continue;
12✔
482
                        }
483
                        if (count($argumentPtrs) > 1) {
12✔
484
                                // Complex argument, we can't handle it, ignore.
485
                                continue;
12✔
486
                        }
487
                        if ($argumentFirstToken['code'] === T_CONSTANT_ENCAPSED_STRING) {
12✔
488
                                // Single-quoted string literal, ie compact('whatever').
489
                                // Substr is to strip the enclosing single-quotes.
490
                                $varName = substr($argumentFirstToken['content'], 1, -1);
12✔
491
                                $variable = new VariableInfo($varName);
12✔
492
                                $variable->firstRead = $argumentPtrs[0];
12✔
493
                                $variablePositionsAndNames[] = $variable;
12✔
494
                                continue;
12✔
495
                        }
496
                        if ($argumentFirstToken['code'] === T_DOUBLE_QUOTED_STRING) {
12✔
497
                                // Double-quoted string literal.
498
                                $regexp = Constants::getDoubleQuotedVarRegexp();
12✔
499
                                if (! empty($regexp) && preg_match($regexp, $argumentFirstToken['content'])) {
12✔
500
                                        // Bail if the string needs variable expansion, that's runtime stuff.
501
                                        continue;
12✔
502
                                }
503
                                // Substr is to strip the enclosing double-quotes.
504
                                $varName = substr($argumentFirstToken['content'], 1, -1);
×
505
                                $variable = new VariableInfo($varName);
×
506
                                $variable->firstRead = $argumentPtrs[0];
×
507
                                $variablePositionsAndNames[] = $variable;
×
508
                                continue;
×
509
                        }
510
                }
6✔
511
                return $variablePositionsAndNames;
12✔
512
        }
513

514
        /**
515
         * Return the token index of the scope start for a token
516
         *
517
         * For a variable within a function body, or a variable within a function
518
         * definition argument list, this will return the function keyword's index.
519
         *
520
         * For a variable within a "use" import list within a function definition,
521
         * this will return the enclosing scope, not the function keyword. This is
522
         * important to note because the "use" keyword performs double-duty, defining
523
         * variables for the function's scope, and consuming the variables in the
524
         * enclosing scope. Use `getUseIndexForUseImport` to determine if this
525
         * token needs to be treated as a "use".
526
         *
527
         * For a variable within an arrow function definition argument list,
528
         * this will return the arrow function's keyword index.
529
         *
530
         * For a variable in an arrow function body, this will return the enclosing
531
         * function's index, which may be incorrect.
532
         *
533
         * Since a variable in an arrow function's body may be imported from the
534
         * enclosing scope, it's important to test to see if the variable is in an
535
         * arrow function and also check its enclosing scope separately.
536
         *
537
         * @param File $phpcsFile
538
         * @param int  $stackPtr
539
         *
540
         * @return ?int
541
         */
542
        public static function findVariableScopeExceptArrowFunctions(File $phpcsFile, $stackPtr)
352✔
543
        {
544
                $tokens = $phpcsFile->getTokens();
352✔
545
                $allowedTypes = [
176✔
546
                        T_VARIABLE,
352✔
547
                        T_DOUBLE_QUOTED_STRING,
352✔
548
                        T_HEREDOC,
352✔
549
                        T_STRING,
352✔
550
                ];
352✔
551
                if (! in_array($tokens[$stackPtr]['code'], $allowedTypes, true)) {
352✔
552
                        throw new \Exception("Cannot find variable scope for non-variable {$tokens[$stackPtr]['type']}");
×
553
                }
554

555
                $startOfTokenScope = self::getStartOfTokenScope($phpcsFile, $stackPtr);
352✔
556
                if (is_int($startOfTokenScope) && $startOfTokenScope > 0) {
352✔
557
                        return $startOfTokenScope;
332✔
558
                }
559

560
                // If there is no "conditions" array, this is a function definition argument.
561
                if (self::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
288✔
562
                        $functionPtr = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
228✔
563
                        if (! is_int($functionPtr)) {
228✔
564
                                throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
565
                        }
566
                        return $functionPtr;
228✔
567
                }
568

569
                self::debug('Cannot find function scope for variable at', $stackPtr);
116✔
570
                return $startOfTokenScope;
116✔
571
        }
572

573
        /**
574
         * Return the token index of the scope start for a variable token
575
         *
576
         * This will only work for a variable within a function's body. Otherwise,
577
         * see `findVariableScope`, which is more complex.
578
         *
579
         * Note that if used on a variable in an arrow function, it will return the
580
         * enclosing function's scope, which may be incorrect.
581
         *
582
         * @param File $phpcsFile
583
         * @param int  $stackPtr
584
         *
585
         * @return ?int
586
         */
587
        private static function getStartOfTokenScope(File $phpcsFile, $stackPtr)
352✔
588
        {
589
                $tokens = $phpcsFile->getTokens();
352✔
590
                $token = $tokens[$stackPtr];
352✔
591

592
                $inClass = false;
352✔
593
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
352✔
594
                $functionTokenTypes = [
176✔
595
                        T_FUNCTION,
352✔
596
                        T_CLOSURE,
352✔
597
                ];
352✔
598
                foreach (array_reverse($conditions, true) as $scopePtr => $scopeCode) {
352✔
599
                        if (in_array($scopeCode, $functionTokenTypes, true) || self::isArrowFunction($phpcsFile, $scopePtr)) {
336✔
600
                                return $scopePtr;
332✔
601
                        }
602
                        if (isset(Tokens::$ooScopeTokens[$scopeCode]) === true) {
160✔
603
                                $inClass = true;
68✔
604
                        }
34✔
605
                }
162✔
606

607
                if ($inClass) {
288✔
608
                        // If this is inside a class and not inside a function, this is either a
609
                        // class member variable definition, or a function argument. If it is a
610
                        // variable definition, it has no scope on its own (it can only be used
611
                        // with an object reference). If it is a function argument, we need to do
612
                        // more work (see `findVariableScopeExceptArrowFunctions`).
613
                        return null;
60✔
614
                }
615

616
                // If we can't find a scope, let's use the first token of the file.
617
                return 0;
248✔
618
        }
619

620
        /**
621
         * @param File $phpcsFile
622
         * @param int  $stackPtr
623
         *
624
         * @return bool
625
         */
626
        public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $stackPtr)
×
627
        {
628
                $tokens = $phpcsFile->getTokens();
×
629
                $token = $tokens[$stackPtr];
×
630
                $openParenIndices = isset($token['nested_parenthesis']) ? $token['nested_parenthesis'] : [];
×
631
                if (empty($openParenIndices)) {
×
632
                        return false;
×
633
                }
634
                $openParenPtr = $openParenIndices[0];
×
635
                return self::isArrowFunction($phpcsFile, $openParenPtr - 1);
×
636
        }
637

638
        /**
639
         * @param File $phpcsFile
640
         * @param int  $stackPtr
641
         * @param int  $enclosingScopeIndex
642
         *
643
         * @return ?int
644
         */
645
        public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
332✔
646
        {
647
                $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
332✔
648
                if (! is_int($arrowFunctionIndex)) {
332✔
649
                        return null;
332✔
650
                }
651
                $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex);
36✔
652
                if (! $arrowFunctionInfo) {
36✔
653
                        return null;
×
654
                }
655

656
                // We found the closest arrow function before this token. If the token is
657
                // within the scope of that arrow function, then return it.
658
                if ($stackPtr > $arrowFunctionInfo['scope_opener'] && $stackPtr < $arrowFunctionInfo['scope_closer']) {
36✔
659
                        return $arrowFunctionIndex;
36✔
660
                }
661

662
                // If the token is after the scope of the closest arrow function, we may
663
                // still be inside the scope of a nested arrow function, so we need to
664
                // search further back until we are certain there are no more arrow
665
                // functions.
666
                if ($stackPtr > $arrowFunctionInfo['scope_closer']) {
36✔
667
                        return self::getContainingArrowFunctionIndex($phpcsFile, $arrowFunctionIndex, $enclosingScopeIndex);
36✔
668
                }
669

UNCOV
670
                return null;
×
671
        }
672

673
        /**
674
         * Move back from the stackPtr to the start of the enclosing scope until we
675
         * find a 'fn' token that starts an arrow function, returning the index of
676
         * that token. Returns null if there are no arrow functions before stackPtr.
677
         *
678
         * Note that this does not guarantee that stackPtr is inside the arrow
679
         * function scope we find!
680
         *
681
         * @param File $phpcsFile
682
         * @param int  $stackPtr
683
         * @param int  $enclosingScopeIndex
684
         *
685
         * @return ?int
686
         */
687
        private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
332✔
688
        {
689
                $tokens = $phpcsFile->getTokens();
332✔
690
                for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) {
332✔
691
                        $token = $tokens[$index];
332✔
692
                        if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) {
332✔
693
                                return $index;
36✔
694
                        }
695
                }
166✔
696
                return null;
332✔
697
        }
698

699
        /**
700
         * @param File $phpcsFile
701
         * @param int  $stackPtr
702
         *
703
         * @return bool
704
         */
705
        public static function isArrowFunction(File $phpcsFile, $stackPtr)
352✔
706
        {
707
                $tokens = $phpcsFile->getTokens();
352✔
708
                if (defined('T_FN') && $tokens[$stackPtr]['code'] === T_FN) {
352✔
709
                        return true;
36✔
710
                }
711
                if ($tokens[$stackPtr]['content'] !== 'fn') {
352✔
712
                        return false;
352✔
713
                }
714
                // Make sure next non-space token is an open parenthesis
715
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
×
716
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
×
717
                        return false;
×
718
                }
719
                // Find the associated close parenthesis
720
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
×
721
                // Make sure the next token is a fat arrow
722
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
×
723
                if (! is_int($fatArrowIndex)) {
×
724
                        return false;
×
725
                }
726
                if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') {
×
727
                        return false;
×
728
                }
729
                return true;
×
730
        }
731

732
        /**
733
         * Find the opening and closing scope positions for an arrow function if the
734
         * given position is the start of the arrow function (the `fn` keyword
735
         * token).
736
         *
737
         * Returns null if the passed token is not an arrow function keyword.
738
         *
739
         * If the token is an arrow function keyword, the scope opener is returned as
740
         * the provided position.
741
         *
742
         * @param File $phpcsFile
743
         * @param int  $stackPtr
744
         *
745
         * @return ?array<string, int>
746
         */
747
        public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
36✔
748
        {
749
                $tokens = $phpcsFile->getTokens();
36✔
750
                if ($tokens[$stackPtr]['content'] !== 'fn') {
36✔
751
                        return null;
×
752
                }
753
                // Make sure next non-space token is an open parenthesis
754
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
36✔
755
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
36✔
756
                        return null;
×
757
                }
758
                // Find the associated close parenthesis
759
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
36✔
760
                // Make sure the next token is a fat arrow or a return type
761
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
36✔
762
                if (! is_int($fatArrowIndex)) {
36✔
763
                        return null;
×
764
                }
765
                if (
766
                        $tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW &&
36✔
767
                        $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW' &&
36✔
768
                        $tokens[$fatArrowIndex]['code'] !== T_COLON
22✔
769
                ) {
18✔
770
                        return null;
×
771
                }
772

773
                // Find the scope closer
774
                $scopeCloserIndex = null;
36✔
775
                $foundCurlyPairs = 0;
36✔
776
                $foundArrayPairs = 0;
36✔
777
                $foundParenPairs = 0;
36✔
778
                $arrowBodyStart = $tokens[$stackPtr]['parenthesis_closer'] + 1;
36✔
779
                $lastToken = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
36✔
780
                for ($index = $arrowBodyStart; $index < $lastToken; $index++) {
36✔
781
                        $token = $tokens[$index];
36✔
782
                        if (empty($token['code'])) {
36✔
783
                                $scopeCloserIndex = $index;
×
784
                                break;
×
785
                        }
786

787
                        $code = $token['code'];
36✔
788

789
                        // A semicolon is always a closer.
790
                        if ($code === T_SEMICOLON) {
36✔
791
                                $scopeCloserIndex = $index;
28✔
792
                                break;
28✔
793
                        }
794

795
                        // Track pair opening tokens.
796
                        if ($code === T_OPEN_CURLY_BRACKET) {
36✔
797
                                $foundCurlyPairs += 1;
8✔
798
                                continue;
8✔
799
                        }
800
                        if ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) {
36✔
801
                                $foundArrayPairs += 1;
28✔
802
                                continue;
28✔
803
                        }
804
                        if ($code === T_OPEN_PARENTHESIS) {
36✔
805
                                $foundParenPairs += 1;
28✔
806
                                continue;
28✔
807
                        }
808

809
                        // A pair closing is only an arrow func closer if there was no matching opening token.
810
                        if ($code === T_CLOSE_CURLY_BRACKET) {
36✔
811
                                if ($foundCurlyPairs === 0) {
×
812
                                        $scopeCloserIndex = $index;
×
813
                                        break;
×
814
                                }
815
                                $foundCurlyPairs -= 1;
×
816
                                continue;
×
817
                        }
818
                        if ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) {
36✔
819
                                if ($foundArrayPairs === 0) {
28✔
820
                                        $scopeCloserIndex = $index;
×
821
                                        break;
×
822
                                }
823
                                $foundArrayPairs -= 1;
28✔
824
                                continue;
28✔
825
                        }
826
                        if ($code === T_CLOSE_PARENTHESIS) {
36✔
827
                                if ($foundParenPairs === 0) {
28✔
828
                                        $scopeCloserIndex = $index;
8✔
829
                                        break;
8✔
830
                                }
831
                                $foundParenPairs -= 1;
28✔
832
                                continue;
28✔
833
                        }
834

835
                        // A comma is a closer only if we are not inside an opening token.
836
                        if ($code === T_COMMA) {
36✔
837
                                if (empty($foundArrayPairs) && empty($foundParenPairs) && empty($foundCurlyPairs)) {
28✔
838
                                        $scopeCloserIndex = $index;
16✔
839
                                        break;
16✔
840
                                }
841
                                continue;
20✔
842
                        }
843
                }
18✔
844

845
                if (! is_int($scopeCloserIndex)) {
36✔
846
                        return null;
×
847
                }
848

849
                return [
18✔
850
                        'scope_opener' => $stackPtr,
36✔
851
                        'scope_closer' => $scopeCloserIndex,
36✔
852
                ];
36✔
853
        }
854

855
        /**
856
         * Determine if a token is a list opener for list assignment/destructuring.
857
         *
858
         * The index provided can be either the opening square brace of a short list
859
         * assignment like the first character of `[$a] = $b;` or the `list` token of
860
         * an expression like `list($a) = $b;` or the opening parenthesis of that
861
         * expression.
862
         *
863
         * @param File $phpcsFile
864
         * @param int  $listOpenerIndex
865
         *
866
         * @return bool
867
         */
868
        private static function isListAssignment(File $phpcsFile, $listOpenerIndex)
32✔
869
        {
870
                $tokens = $phpcsFile->getTokens();
32✔
871
                // Match `[$a] = $b;` except for when the previous token is a parenthesis.
872
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SHORT_ARRAY) {
32✔
873
                        return true;
28✔
874
                }
875
                // Match `list($a) = $b;`
876
                if ($tokens[$listOpenerIndex]['code'] === T_LIST) {
32✔
877
                        return true;
32✔
878
                }
879

880
                // If $listOpenerIndex is the open parenthesis of `list($a) = $b;`, then
881
                // match that too.
882
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_PARENTHESIS) {
16✔
883
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
4✔
884
                        if (
885
                                isset($tokens[$previousTokenPtr])
4✔
886
                                && $tokens[$previousTokenPtr]['code'] === T_LIST
4✔
887
                        ) {
2✔
888
                                return true;
4✔
889
                        }
890
                        return true;
×
891
                }
892

893
                // If the list opener token is a square bracket that is preceeded by a
894
                // close parenthesis that has an owner which is a scope opener, then this
895
                // is a list assignment and not an array access.
896
                //
897
                // Match `if (true) [$a] = $b;`
898
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SQUARE_BRACKET) {
12✔
899
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
12✔
900
                        if (
901
                                isset($tokens[$previousTokenPtr])
12✔
902
                                && $tokens[$previousTokenPtr]['code'] === T_CLOSE_PARENTHESIS
12✔
903
                                && isset($tokens[$previousTokenPtr]['parenthesis_owner'])
12✔
904
                                && isset(Tokens::$scopeOpeners[$tokens[$tokens[$previousTokenPtr]['parenthesis_owner']]['code']])
12✔
905
                        ) {
6✔
906
                                return true;
4✔
907
                        }
908
                }
4✔
909

910
                return false;
8✔
911
        }
912

913
        /**
914
         * Return a list of indices for variables assigned within a list assignment.
915
         *
916
         * The index provided can be either the opening square brace of a short list
917
         * assignment like the first character of `[$a] = $b;` or the `list` token of
918
         * an expression like `list($a) = $b;` or the opening parenthesis of that
919
         * expression.
920
         *
921
         * @param File $phpcsFile
922
         * @param int  $listOpenerIndex
923
         *
924
         * @return ?array<int>
925
         */
926
        public static function getListAssignments(File $phpcsFile, $listOpenerIndex)
48✔
927
        {
928
                $tokens = $phpcsFile->getTokens();
48✔
929
                self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]);
48✔
930

931
                // First find the end of the list
932
                $closePtr = null;
48✔
933
                if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) {
48✔
934
                        $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer'];
48✔
935
                }
24✔
936
                if (isset($tokens[$listOpenerIndex]['bracket_closer'])) {
48✔
937
                        $closePtr = $tokens[$listOpenerIndex]['bracket_closer'];
48✔
938
                }
24✔
939
                if (! $closePtr) {
48✔
940
                        return null;
×
941
                }
942

943
                // Find the assignment (equals sign) which, if this is a list assignment, should be the next non-space token
944
                $assignPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $closePtr + 1, null, true);
48✔
945

946
                // If the next token isn't an assignment, check for nested brackets because we might be a nested assignment
947
                if (! is_int($assignPtr) || $tokens[$assignPtr]['code'] !== T_EQUAL) {
48✔
948
                        // Collect the enclosing list open/close tokens ($parents is an assoc array keyed by opener index and the value is the closer index)
949
                        $parents = isset($tokens[$listOpenerIndex]['nested_parenthesis']) ? $tokens[$listOpenerIndex]['nested_parenthesis'] : [];
40✔
950
                        // There's no record of nested brackets for short lists; we'll have to find the parent ourselves
951
                        if (empty($parents)) {
40✔
952
                                $parentSquareBracketPtr = self::findContainingOpeningSquareBracket($phpcsFile, $listOpenerIndex);
40✔
953
                                if (is_int($parentSquareBracketPtr)) {
40✔
954
                                        // Make sure that the parent is really a parent by checking that its
955
                                        // closing index is outside of the current bracket's closing index.
956
                                        $parentSquareBracketToken = $tokens[$parentSquareBracketPtr];
4✔
957
                                        $parentSquareBracketClosePtr = $parentSquareBracketToken['bracket_closer'];
4✔
958
                                        if ($parentSquareBracketClosePtr && $parentSquareBracketClosePtr > $closePtr) {
4✔
959
                                                self::debug("found enclosing bracket for {$listOpenerIndex}: {$parentSquareBracketPtr}");
4✔
960
                                                // Collect the opening index, but we don't actually need the closing paren index so just make that 0
961
                                                $parents = [$parentSquareBracketPtr => 0];
4✔
962
                                        }
2✔
963
                                }
2✔
964
                        }
20✔
965
                        // If we have no parents, this is not a nested assignment and therefore is not an assignment
966
                        if (empty($parents)) {
40✔
967
                                return null;
40✔
968
                        }
969

970
                        // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion)
971
                        $isNestedAssignment = null;
28✔
972
                        $parentListOpener = array_keys(array_reverse($parents, true))[0];
28✔
973
                        $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener);
28✔
974
                        if ($isNestedAssignment === null) {
28✔
975
                                return null;
28✔
976
                        }
977
                }
2✔
978

979
                $variablePtrs = [];
32✔
980

981
                $currentPtr = $listOpenerIndex;
32✔
982
                $variablePtr = 0;
32✔
983
                while ($currentPtr < $closePtr && is_int($variablePtr)) {
32✔
984
                        $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr);
32✔
985
                        if (is_int($variablePtr)) {
32✔
986
                                $variablePtrs[] = $variablePtr;
32✔
987
                        }
16✔
988
                        ++$currentPtr;
32✔
989
                }
16✔
990

991
                if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) {
32✔
992
                        return null;
8✔
993
                }
994

995
                return $variablePtrs;
32✔
996
        }
997

998
        /**
999
         * @param File $phpcsFile
1000
         * @param int  $stackPtr
1001
         *
1002
         * @return string[]
1003
         */
1004
        public static function getVariablesDefinedByArrowFunction(File $phpcsFile, $stackPtr)
36✔
1005
        {
1006
                $tokens = $phpcsFile->getTokens();
36✔
1007
                $arrowFunctionToken = $tokens[$stackPtr];
36✔
1008
                $variableNames = [];
36✔
1009
                self::debug('looking for variables in arrow function token', $arrowFunctionToken);
36✔
1010
                for ($index = $arrowFunctionToken['parenthesis_opener']; $index < $arrowFunctionToken['parenthesis_closer']; $index++) {
36✔
1011
                        $token = $tokens[$index];
36✔
1012
                        if ($token['code'] === T_VARIABLE) {
36✔
1013
                                $variableNames[] = self::normalizeVarName($token['content']);
36✔
1014
                        }
18✔
1015
                }
18✔
1016
                self::debug('found these variables in arrow function token', $variableNames);
36✔
1017
                return $variableNames;
36✔
1018
        }
1019

1020
        /**
1021
         * @return void
1022
         */
1023
        public static function debug()
352✔
1024
        {
1025
                $messages = func_get_args();
352✔
1026
                if (! defined('PHP_CODESNIFFER_VERBOSITY')) {
352✔
1027
                        return;
×
1028
                }
1029
                if (PHP_CODESNIFFER_VERBOSITY <= 3) {
352✔
1030
                        return;
352✔
1031
                }
1032
                $output = PHP_EOL . 'VariableAnalysisSniff: DEBUG:';
×
1033
                foreach ($messages as $message) {
×
1034
                        if (is_string($message) || is_numeric($message)) {
×
1035
                                $output .= ' "' . $message . '"';
×
1036
                                continue;
×
1037
                        }
1038
                        $output .= PHP_EOL . var_export($message, true) . PHP_EOL;
×
1039
                }
1040
                $output .= PHP_EOL;
×
1041
                echo $output;
×
1042
        }
1043

1044
        /**
1045
         * @param string $pattern
1046
         * @param string $value
1047
         *
1048
         * @return string[]
1049
         */
1050
        public static function splitStringToArray($pattern, $value)
24✔
1051
        {
1052
                if (empty($pattern)) {
24✔
1053
                        return [];
×
1054
                }
1055
                $result = preg_split($pattern, $value);
24✔
1056
                return is_array($result) ? $result : [];
24✔
1057
        }
1058

1059
        /**
1060
         * @param string $varName
1061
         *
1062
         * @return bool
1063
         */
1064
        public static function isVariableANumericVariable($varName)
336✔
1065
        {
1066
                return is_numeric(substr($varName, 0, 1));
336✔
1067
        }
1068

1069
        /**
1070
         * @param File $phpcsFile
1071
         * @param int  $stackPtr
1072
         *
1073
         * @return bool
1074
         */
1075
        public static function isVariableInsideElseCondition(File $phpcsFile, $stackPtr)
328✔
1076
        {
1077
                $tokens = $phpcsFile->getTokens();
328✔
1078
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
328✔
1079
                $nonFunctionTokenTypes[] = T_OPEN_PARENTHESIS;
328✔
1080
                $nonFunctionTokenTypes[] = T_INLINE_HTML;
328✔
1081
                $nonFunctionTokenTypes[] = T_CLOSE_TAG;
328✔
1082
                $nonFunctionTokenTypes[] = T_VARIABLE;
328✔
1083
                $nonFunctionTokenTypes[] = T_ELLIPSIS;
328✔
1084
                $nonFunctionTokenTypes[] = T_COMMA;
328✔
1085
                $nonFunctionTokenTypes[] = T_STRING;
328✔
1086
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
328✔
1087
                $elsePtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $stackPtr - 1, null, true, null, true));
328✔
1088
                $elseTokenTypes = [
164✔
1089
                        T_ELSE,
328✔
1090
                        T_ELSEIF,
328✔
1091
                ];
328✔
1092
                if (is_int($elsePtr) && in_array($tokens[$elsePtr]['code'], $elseTokenTypes, true)) {
328✔
1093
                        return true;
16✔
1094
                }
1095
                return false;
328✔
1096
        }
1097

1098
        /**
1099
         * @param File $phpcsFile
1100
         * @param int  $stackPtr
1101
         *
1102
         * @return bool
1103
         */
1104
        public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr)
328✔
1105
        {
1106
                $tokens = $phpcsFile->getTokens();
328✔
1107
                $token = $tokens[$stackPtr];
328✔
1108
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
328✔
1109
                $elseTokenTypes = [
164✔
1110
                        T_ELSE,
328✔
1111
                        T_ELSEIF,
328✔
1112
                ];
328✔
1113
                foreach (array_reverse($conditions, true) as $scopeCode) {
328✔
1114
                        if (in_array($scopeCode, $elseTokenTypes, true)) {
308✔
1115
                                return true;
16✔
1116
                        }
1117
                }
164✔
1118

1119
                // Some else body code will not have conditions because it is inline (no
1120
                // curly braces) so we have to look in other ways.
1121
                $previousSemicolonPtr = $phpcsFile->findPrevious([T_SEMICOLON], $stackPtr - 1);
328✔
1122
                if (! is_int($previousSemicolonPtr)) {
328✔
1123
                        $previousSemicolonPtr = 0;
148✔
1124
                }
74✔
1125
                $elsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1, $previousSemicolonPtr);
328✔
1126
                if (is_int($elsePtr)) {
328✔
1127
                        return true;
8✔
1128
                }
1129

1130
                return false;
328✔
1131
        }
1132

1133
        /**
1134
         * @param File $phpcsFile
1135
         * @param int  $stackPtr
1136
         *
1137
         * @return int[]
1138
         */
1139
        public static function getAttachedBlockIndicesForElse(File $phpcsFile, $stackPtr)
24✔
1140
        {
1141
                $currentElsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1);
24✔
1142
                if (! is_int($currentElsePtr)) {
24✔
1143
                        throw new \Exception("Cannot find expected else at {$stackPtr}");
×
1144
                }
1145

1146
                $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
24✔
1147
                if (! is_int($ifPtr)) {
24✔
1148
                        throw new \Exception("Cannot find if for else at {$stackPtr}");
×
1149
                }
1150
                $blockIndices = [$ifPtr];
24✔
1151

1152
                $previousElseIfPtr = $currentElsePtr;
24✔
1153
                do {
1154
                        $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
24✔
1155
                        if (is_int($elseIfPtr)) {
24✔
1156
                                $blockIndices[] = $elseIfPtr;
16✔
1157
                                $previousElseIfPtr = $elseIfPtr;
16✔
1158
                        }
8✔
1159
                } while (is_int($elseIfPtr));
24✔
1160

1161
                return $blockIndices;
24✔
1162
        }
1163

1164
        /**
1165
         * @param int $needle
1166
         * @param int $scopeStart
1167
         * @param int $scopeEnd
1168
         *
1169
         * @return bool
1170
         */
1171
        public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
24✔
1172
        {
1173
                return ($needle > $scopeStart && $needle < $scopeEnd);
24✔
1174
        }
1175

1176
        /**
1177
         * @param File $phpcsFile
1178
         * @param int  $scopeStartIndex
1179
         *
1180
         * @return int
1181
         */
1182
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
352✔
1183
        {
1184
                $tokens = $phpcsFile->getTokens();
352✔
1185
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
352✔
1186

1187
                if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) {
352✔
1188
                        $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex);
36✔
1189
                        $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex;
36✔
1190
                }
18✔
1191

1192
                if ($scopeStartIndex === 0) {
352✔
1193
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
352✔
1194
                }
176✔
1195
                return $scopeCloserIndex;
352✔
1196
        }
1197

1198
        /**
1199
         * @param File $phpcsFile
1200
         *
1201
         * @return int
1202
         */
1203
        public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
352✔
1204
        {
1205
                $tokens = $phpcsFile->getTokens();
352✔
1206
                foreach (array_reverse($tokens, true) as $index => $token) {
352✔
1207
                        if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
352✔
1208
                                return $index;
352✔
1209
                        }
1210
                }
172✔
1211
                self::debug('no non-empty token found for end of file');
×
1212
                return 0;
×
1213
        }
1214

1215
        /**
1216
         * @param VariableInfo $varInfo
1217
         * @param ScopeInfo    $scopeInfo
1218
         *
1219
         * @return bool
1220
         */
1221
        public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo)
64✔
1222
        {
1223
                $foundVarPosition = false;
64✔
1224
                foreach ($scopeInfo->variables as $variable) {
64✔
1225
                        if ($variable === $varInfo) {
64✔
1226
                                $foundVarPosition = true;
64✔
1227
                                continue;
64✔
1228
                        }
1229
                        if (! $foundVarPosition) {
44✔
1230
                                continue;
36✔
1231
                        }
1232
                        if ($variable->scopeType !== ScopeType::PARAM) {
44✔
1233
                                continue;
36✔
1234
                        }
1235
                        if ($variable->firstRead) {
16✔
1236
                                return true;
16✔
1237
                        }
1238
                }
32✔
1239
                return false;
64✔
1240
        }
1241

1242
        /**
1243
         * @param File         $phpcsFile
1244
         * @param VariableInfo $varInfo
1245
         * @param ScopeInfo    $scopeInfo
1246
         *
1247
         * @return bool
1248
         */
1249
        public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
4✔
1250
        {
1251
                $requireTokens = [
2✔
1252
                        T_REQUIRE,
4✔
1253
                        T_REQUIRE_ONCE,
4✔
1254
                        T_INCLUDE,
4✔
1255
                        T_INCLUDE_ONCE,
4✔
1256
                ];
4✔
1257
                $indexToStartSearch = $varInfo->firstDeclared;
4✔
1258
                if (! empty($varInfo->firstInitialized)) {
4✔
1259
                        $indexToStartSearch = $varInfo->firstInitialized;
4✔
1260
                }
2✔
1261
                $tokens = $phpcsFile->getTokens();
4✔
1262
                $indexToStopSearch = isset($tokens[$scopeInfo->scopeStartIndex]['scope_closer']) ? $tokens[$scopeInfo->scopeStartIndex]['scope_closer'] : null;
4✔
1263
                if (! is_int($indexToStartSearch) || ! is_int($indexToStopSearch)) {
4✔
1264
                        return false;
×
1265
                }
1266
                $requireTokenIndex = $phpcsFile->findNext($requireTokens, $indexToStartSearch + 1, $indexToStopSearch);
4✔
1267
                if (is_int($requireTokenIndex)) {
4✔
1268
                        return true;
4✔
1269
                }
1270
                return false;
×
1271
        }
1272

1273
        /**
1274
         * Find the index of the function keyword for a token in a function call's arguments
1275
         *
1276
         * For the variable `$foo` in the expression `doSomething($foo)`, this will
1277
         * return the index of the `doSomething` token.
1278
         *
1279
         * @param File $phpcsFile
1280
         * @param int  $stackPtr
1281
         *
1282
         * @return ?int
1283
         */
1284
        public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr)
340✔
1285
        {
1286
                $tokens = $phpcsFile->getTokens();
340✔
1287
                $token = $tokens[$stackPtr];
340✔
1288
                if (empty($token['nested_parenthesis'])) {
340✔
1289
                        return null;
332✔
1290
                }
1291
                /**
1292
                 * @var array<int|string|null>
1293
                 */
1294
                $startingParenthesis = array_keys($token['nested_parenthesis']);
212✔
1295
                $startOfArguments = end($startingParenthesis);
212✔
1296
                if (! is_int($startOfArguments)) {
212✔
1297
                        return null;
×
1298
                }
1299

1300
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
212✔
1301
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
212✔
1302
                if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
212✔
1303
                        return null;
×
1304
                }
1305
                if (
1306
                        $tokens[$functionPtr]['content'] === 'function'
212✔
1307
                        || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))
212✔
1308
                ) {
106✔
1309
                        // If there is a function/fn keyword before the beginning of the parens,
1310
                        // this is a function definition and not a function call.
1311
                        return null;
×
1312
                }
1313
                if (! empty($tokens[$functionPtr]['scope_opener'])) {
212✔
1314
                        // If the alleged function name has a scope, this is not a function call.
1315
                        return null;
138✔
1316
                }
1317

1318
                $functionNameType = $tokens[$functionPtr]['code'];
178✔
1319
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
178✔
1320
                        // If the alleged function name is not a variable or a string, this is
1321
                        // not a function call.
1322
                        return null;
48✔
1323
                }
1324

1325
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
174✔
1326
                        // If the variable is inside a different scope than the function name,
1327
                        // the function call doesn't apply to the variable.
1328
                        return null;
28✔
1329
                }
1330

1331
                return $functionPtr;
174✔
1332
        }
1333

1334
        /**
1335
         * @param File $phpcsFile
1336
         * @param int  $stackPtr
1337
         *
1338
         * @return bool
1339
         */
1340
        public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
328✔
1341
        {
1342
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
328✔
1343
                if (! is_int($functionIndex)) {
328✔
1344
                        return false;
310✔
1345
                }
1346
                $tokens = $phpcsFile->getTokens();
174✔
1347
                if (! isset($tokens[$functionIndex])) {
174✔
1348
                        return false;
×
1349
                }
1350
                $allowedFunctionNames = [
87✔
1351
                        'isset',
174✔
1352
                        'empty',
174✔
1353
                ];
174✔
1354
                if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) {
174✔
1355
                        return true;
20✔
1356
                }
1357
                return false;
166✔
1358
        }
1359

1360
        /**
1361
         * @param File $phpcsFile
1362
         * @param int  $stackPtr
1363
         *
1364
         * @return bool
1365
         */
1366
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
256✔
1367
        {
1368
                $tokens = $phpcsFile->getTokens();
256✔
1369
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
256✔
1370

1371
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
256✔
1372
                if (! is_int($arrayPushOperatorIndex1)) {
256✔
1373
                        return false;
×
1374
                }
1375
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
256✔
1376
                        return false;
256✔
1377
                }
1378

1379
                $arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true));
36✔
1380
                if (! is_int($arrayPushOperatorIndex2)) {
36✔
1381
                        return false;
×
1382
                }
1383
                if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') {
36✔
1384
                        return false;
8✔
1385
                }
1386

1387
                $arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true));
28✔
1388
                if (! is_int($arrayPushOperatorIndex3)) {
28✔
1389
                        return false;
×
1390
                }
1391
                if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') {
28✔
1392
                        return false;
×
1393
                }
1394

1395
                return true;
28✔
1396
        }
1397

1398
        /**
1399
         * @param File $phpcsFile
1400
         * @param int  $stackPtr
1401
         *
1402
         * @return bool
1403
         */
1404
        public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
256✔
1405
        {
1406
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
256✔
1407
                if (! is_int($functionIndex)) {
256✔
1408
                        return false;
226✔
1409
                }
1410
                $tokens = $phpcsFile->getTokens();
98✔
1411
                if (! isset($tokens[$functionIndex])) {
98✔
1412
                        return false;
×
1413
                }
1414
                if ($tokens[$functionIndex]['content'] === 'unset') {
98✔
1415
                        return true;
8✔
1416
                }
1417
                return false;
90✔
1418
        }
1419

1420
        /**
1421
         * @param File $phpcsFile
1422
         * @param int  $stackPtr
1423
         *
1424
         * @return bool
1425
         */
1426
        public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr)
320✔
1427
        {
1428
                $previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
320✔
1429
                if (! is_int($previousStatementPtr)) {
320✔
1430
                        $previousStatementPtr = 1;
40✔
1431
                }
20✔
1432
                $previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
320✔
1433
                if (is_int($previousTokenPtr)) {
320✔
1434
                        return true;
4✔
1435
                }
1436
                return false;
320✔
1437
        }
1438

1439
        /**
1440
         * @param File $phpcsFile
1441
         * @param int  $stackPtr
1442
         *
1443
         * @return bool
1444
         */
1445
        public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
340✔
1446
        {
1447
                // Is the next non-whitespace an assignment?
1448
                $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
340✔
1449
                if (! is_int($assignPtr)) {
340✔
1450
                        return false;
332✔
1451
                }
1452

1453
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1454
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
320✔
1455
                        self::debug('found variable variable');
4✔
1456
                        return false;
4✔
1457
                }
1458
                return true;
320✔
1459
        }
1460

1461
        /**
1462
         * @param File $phpcsFile
1463
         * @param int  $stackPtr
1464
         *
1465
         * @return bool
1466
         */
1467
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
320✔
1468
        {
1469
                $tokens = $phpcsFile->getTokens();
320✔
1470

1471
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
320✔
1472
                if ($prev === false) {
320✔
1473
                        return false;
×
1474
                }
1475
                if ($tokens[$prev]['code'] === T_DOLLAR) {
320✔
1476
                        return true;
4✔
1477
                }
1478
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
320✔
1479
                        return false;
264✔
1480
                }
1481

1482
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
228✔
1483
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
228✔
1484
                        return true;
×
1485
                }
1486
                return false;
228✔
1487
        }
1488

1489
        /**
1490
         * @param File $phpcsFile
1491
         * @param int  $stackPtr
1492
         *
1493
         * @return EnumInfo|null
1494
         */
1495
        public static function makeEnumInfo(File $phpcsFile, $stackPtr)
4✔
1496
        {
1497
                $tokens = $phpcsFile->getTokens();
4✔
1498
                $token = $tokens[$stackPtr];
4✔
1499

1500
                if (isset($token['scope_opener'])) {
4✔
1501
                        $blockStart = $token['scope_opener'];
2✔
1502
                        $blockEnd = $token['scope_closer'];
2✔
1503
                } else {
1✔
1504
                        // Enums before phpcs could detect them do not have scopes so we have to
1505
                        // find them ourselves.
1506

1507
                        $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
4✔
1508
                        if (! is_int($blockStart)) {
4✔
1509
                                return null;
4✔
1510
                        }
1511
                        $blockEnd = $tokens[$blockStart]['bracket_closer'];
2✔
1512
                }
1513

1514
                return new EnumInfo(
4✔
1515
                        $stackPtr,
4✔
1516
                        $blockStart,
4✔
1517
                        $blockEnd
2✔
1518
                );
4✔
1519
        }
1520

1521
        /**
1522
         * @param File $phpcsFile
1523
         * @param int  $stackPtr
1524
         *
1525
         * @return ForLoopInfo
1526
         */
1527
        public static function makeForLoopInfo(File $phpcsFile, $stackPtr)
8✔
1528
        {
1529
                $tokens = $phpcsFile->getTokens();
8✔
1530
                $token = $tokens[$stackPtr];
8✔
1531
                $forIndex = $stackPtr;
8✔
1532
                $blockStart = $token['parenthesis_closer'];
8✔
1533
                if (isset($token['scope_opener'])) {
8✔
1534
                        $blockStart = $token['scope_opener'];
8✔
1535
                        $blockEnd = $token['scope_closer'];
8✔
1536
                } else {
4✔
1537
                        // Some for loop blocks will not have scope positions because it they are
1538
                        // inline (no curly braces) so we have to find the end of their scope by
1539
                        // looking for the end of the next statement.
1540
                        $nextSemicolonIndex = $phpcsFile->findNext([T_SEMICOLON], $token['parenthesis_closer']);
8✔
1541
                        if (! is_int($nextSemicolonIndex)) {
8✔
1542
                                $nextSemicolonIndex = $token['parenthesis_closer'] + 1;
×
1543
                        }
1544
                        $blockEnd = $nextSemicolonIndex;
8✔
1545
                }
1546
                $initStart = intval($token['parenthesis_opener']) + 1;
8✔
1547
                $initEnd = null;
8✔
1548
                $conditionStart = null;
8✔
1549
                $conditionEnd = null;
8✔
1550
                $incrementStart = null;
8✔
1551
                $incrementEnd = $token['parenthesis_closer'] - 1;
8✔
1552

1553
                $semicolonCount = 0;
8✔
1554
                $forLoopLevel = $tokens[$forIndex]['level'];
8✔
1555
                $forLoopNestedParensCount = 1;
8✔
1556

1557
                if (isset($tokens[$forIndex]['nested_parenthesis'])) {
8✔
1558
                        $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
×
1559
                }
1560

1561
                for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
8✔
1562
                        if ($tokens[$i]['code'] !== T_SEMICOLON) {
8✔
1563
                                continue;
8✔
1564
                        }
1565

1566
                        if ($tokens[$i]['level'] !== $forLoopLevel) {
8✔
1567
                                continue;
8✔
1568
                        }
1569

1570
                        if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
8✔
1571
                                continue;
×
1572
                        }
1573

1574
                        switch ($semicolonCount) {
1575
                                case 0:
8✔
1576
                                        $initEnd = $i;
8✔
1577
                                        $conditionStart = $initEnd + 1;
8✔
1578
                                        break;
8✔
1579
                                case 1:
8✔
1580
                                        $conditionEnd = $i;
8✔
1581
                                        $incrementStart = $conditionEnd + 1;
8✔
1582
                                        break;
8✔
1583
                        }
1584
                        $semicolonCount += 1;
8✔
1585
                }
4✔
1586

1587
                if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
8✔
1588
                        throw new \Exception("Cannot parse for loop at position {$forIndex}");
×
1589
                }
1590

1591
                return new ForLoopInfo(
8✔
1592
                        $forIndex,
8✔
1593
                        $blockStart,
8✔
1594
                        $blockEnd,
8✔
1595
                        $initStart,
8✔
1596
                        $initEnd,
8✔
1597
                        $conditionStart,
8✔
1598
                        $conditionEnd,
8✔
1599
                        $incrementStart,
8✔
1600
                        $incrementEnd
4✔
1601
                );
8✔
1602
        }
1603

1604
        /**
1605
         * @param int                     $stackPtr
1606
         * @param array<int, ForLoopInfo> $forLoops
1607
         * @return ForLoopInfo|null
1608
         */
1609
        public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
348✔
1610
        {
1611
                foreach ($forLoops as $forLoop) {
348✔
1612
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1613
                                return $forLoop;
8✔
1614
                        }
1615
                }
174✔
1616
                return null;
348✔
1617
        }
1618

1619
        /**
1620
         * Return true if the token looks like constructor promotion.
1621
         *
1622
         * Call on a parameter variable token only.
1623
         *
1624
         * @param File $phpcsFile
1625
         * @param int  $stackPtr
1626
         *
1627
         * @return bool
1628
         */
1629
        public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
228✔
1630
        {
1631
                // If we are not in a function's parameters, this is not promotion.
1632
                $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
228✔
1633
                if (! $functionIndex) {
228✔
1634
                        return false;
×
1635
                }
1636

1637
                $tokens = $phpcsFile->getTokens();
228✔
1638

1639
                // Move backwards from the token, ignoring whitespace, typehints, and the
1640
                // 'readonly' keyword, and return true if the previous token is a
1641
                // visibility keyword (eg: `public`).
1642
                for ($i = $stackPtr - 1; $i > $functionIndex; $i--) {
228✔
1643
                        if (in_array($tokens[$i]['code'], Tokens::$scopeModifiers, true)) {
228✔
1644
                                return true;
12✔
1645
                        }
1646
                        if (in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) {
228✔
1647
                                continue;
156✔
1648
                        }
1649
                        if ($tokens[$i]['content'] === 'readonly') {
228✔
1650
                                continue;
8✔
1651
                        }
1652
                        if (self::isTokenPartOfTypehint($phpcsFile, $i)) {
228✔
1653
                                continue;
32✔
1654
                        }
1655
                        return false;
224✔
1656
                }
1657
                return false;
×
1658
        }
1659

1660
        /**
1661
         * Return false if the token is definitely not part of a typehint
1662
         *
1663
         * @param File $phpcsFile
1664
         * @param int  $stackPtr
1665
         *
1666
         * @return bool
1667
         */
1668
        private static function isTokenPossiblyPartOfTypehint(File $phpcsFile, $stackPtr)
228✔
1669
        {
1670
                $tokens = $phpcsFile->getTokens();
228✔
1671
                $token = $tokens[$stackPtr];
228✔
1672
                if ($token['code'] === 'PHPCS_T_NULLABLE') {
228✔
1673
                        return true;
8✔
1674
                }
1675
                if ($token['code'] === T_NS_SEPARATOR) {
228✔
1676
                        return true;
16✔
1677
                }
1678
                if ($token['code'] === T_STRING) {
228✔
1679
                        return true;
32✔
1680
                }
1681
                if ($token['code'] === T_TRUE) {
228✔
1682
                        return true;
8✔
1683
                }
1684
                if ($token['code'] === T_FALSE) {
228✔
1685
                        return true;
8✔
1686
                }
1687
                if ($token['code'] === T_NULL) {
228✔
1688
                        return true;
8✔
1689
                }
1690
                if ($token['content'] === '|') {
228✔
1691
                        return true;
8✔
1692
                }
1693
                if (in_array($token['code'], Tokens::$emptyTokens)) {
228✔
1694
                        return true;
32✔
1695
                }
1696
                return false;
228✔
1697
        }
1698

1699
        /**
1700
         * Return true if the token is inside a typehint
1701
         *
1702
         * @param File $phpcsFile
1703
         * @param int  $stackPtr
1704
         *
1705
         * @return bool
1706
         */
1707
        public static function isTokenPartOfTypehint(File $phpcsFile, $stackPtr)
228✔
1708
        {
1709
                $tokens = $phpcsFile->getTokens();
228✔
1710

1711
                if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $stackPtr)) {
228✔
1712
                        return false;
224✔
1713
                }
1714

1715
                // Examine every following token, ignoring everything that might be part of
1716
                // a typehint. If we find a variable at the end, this is part of a
1717
                // typehint.
1718
                $i = $stackPtr;
32✔
1719
                while (true) {
32✔
1720
                        $i += 1;
32✔
1721
                        if (! isset($tokens[$i])) {
32✔
1722
                                return false;
×
1723
                        }
1724
                        if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $i)) {
32✔
1725
                                return ($tokens[$i]['code'] === T_VARIABLE);
32✔
1726
                        }
1727
                }
16✔
1728
        }
1729

1730
        /**
1731
         * Return true if the token is inside an abstract class.
1732
         *
1733
         * @param File $phpcsFile
1734
         * @param int  $stackPtr
1735
         *
1736
         * @return bool
1737
         */
1738
        public static function isInAbstractClass(File $phpcsFile, $stackPtr)
108✔
1739
        {
1740
                $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
108✔
1741
                if (! is_int($classIndex)) {
108✔
1742
                        return false;
92✔
1743
                }
1744
                $classProperties = $phpcsFile->getClassProperties($classIndex);
16✔
1745
                return $classProperties['is_abstract'];
16✔
1746
        }
1747

1748
        /**
1749
         * Return true if the function body is empty or contains only `return;`
1750
         *
1751
         * @param File $phpcsFile
1752
         * @param int  $stackPtr  The index of the function keyword.
1753
         *
1754
         * @return bool
1755
         */
1756
        public static function isFunctionBodyEmpty(File $phpcsFile, $stackPtr)
8✔
1757
        {
1758
                $tokens = $phpcsFile->getTokens();
8✔
1759
                if ($tokens[$stackPtr]['code'] !== T_FUNCTION) {
8✔
1760
                        return false;
×
1761
                }
1762
                $functionScopeStart = $tokens[$stackPtr]['scope_opener'];
8✔
1763
                $functionScopeEnd = $tokens[$stackPtr]['scope_closer'];
8✔
1764
                $tokensToIgnore = array_merge(
8✔
1765
                        Tokens::$emptyTokens,
8✔
1766
                        [
4✔
1767
                                T_RETURN,
8✔
1768
                                T_SEMICOLON,
8✔
1769
                                T_OPEN_CURLY_BRACKET,
8✔
1770
                                T_CLOSE_CURLY_BRACKET,
8✔
1771
                        ]
4✔
1772
                );
8✔
1773
                for ($i = $functionScopeStart; $i < $functionScopeEnd; $i++) {
8✔
1774
                        if (! in_array($tokens[$i]['code'], $tokensToIgnore, true)) {
8✔
1775
                                return false;
8✔
1776
                        }
1777
                }
4✔
1778
                return true;
8✔
1779
        }
1780
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc