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

sirbrillig / phpcs-variable-analysis / 18144438392

30 Sep 2025 09:51PM UTC coverage: 93.382% (-0.4%) from 93.79%
18144438392

Pull #356

github

sirbrillig
Remove phpunit 12
Pull Request #356: Update for phpcs 4.0.0

41 of 52 new or added lines in 2 files covered. (78.85%)

80 existing lines in 1 file now uncovered.

1933 of 2070 relevant lines covered (93.38%)

132.36 hits per line

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

89.29
/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()
356✔
20
        {
21
                return array_merge(
356✔
22
                        array_values(Tokens::$emptyTokens),
356✔
23
                        [
178✔
24
                                T_INLINE_HTML,
356✔
25
                                T_CLOSE_TAG,
356✔
26
                        ]
178✔
27
                );
267✔
28
        }
29

30
        /**
31
         * @param int|bool $value
32
         *
33
         * @return ?int
34
         */
35
        public static function getIntOrNull($value)
352✔
36
        {
37
                return is_int($value) ? $value : null;
352✔
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)
336✔
50
        {
51
                // Find the previous bracket within this same statement.
52
                $previousStatementPtr = self::getPreviousStatementPtr($phpcsFile, $stackPtr);
336✔
53
                $openBracketPosition = self::getIntOrNull($phpcsFile->findPrevious([T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET], $stackPtr - 1, $previousStatementPtr));
336✔
54
                if (empty($openBracketPosition)) {
336✔
55
                        return null;
336✔
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)
336✔
80
        {
81
                $result = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET], $stackPtr - 1);
336✔
82
                return is_bool($result) ? 1 : $result;
336✔
83
        }
84

85
        /**
86
         * @param File $phpcsFile
87
         * @param int  $stackPtr
88
         *
89
         * @return ?int
90
         */
91
        public static function findContainingOpeningBracket(File $phpcsFile, $stackPtr)
352✔
92
        {
93
                $tokens = $phpcsFile->getTokens();
352✔
94
                if (isset($tokens[$stackPtr]['nested_parenthesis'])) {
352✔
95
                        /**
96
                         * @var list<int|string>
97
                         */
98
                        $openPtrs = array_keys($tokens[$stackPtr]['nested_parenthesis']);
260✔
99
                        return (int)end($openPtrs);
260✔
100
                }
101
                return null;
348✔
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
                }
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
                }
10✔
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)
328✔
134
        {
135
                $conditions = $token['conditions'];
328✔
136
                $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
328✔
137
                if (defined('T_ENUM')) {
328✔
138
                        $classlikeCodes[] = T_ENUM;
164✔
139
                }
140
                $classlikeCodes[] = 'PHPCS_T_ENUM';
328✔
141
                foreach (array_reverse($conditions, true) as $scopeCode) {
328✔
142
                        if (in_array($scopeCode, $classlikeCodes)) {
328✔
143
                                return false;
4✔
144
                        }
145
                        if ($scopeCode === T_FUNCTION) {
328✔
146
                                return true;
324✔
147
                        }
148
                }
32✔
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
                ];
6✔
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)
356✔
185
        {
186
                return is_int(self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr));
356✔
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)
324✔
201
        {
202
                return is_int(self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr));
324✔
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)
356✔
223
        {
224
                $tokens = $phpcsFile->getTokens();
356✔
225
                $token = $tokens[$stackPtr];
356✔
226
                if ($token['code'] === 'PHPCS_T_OPEN_PARENTHESIS') {
356✔
227
                        $startOfArguments = $stackPtr;
×
228
                } elseif ($token['code'] === 'PHPCS_T_CLOSE_PARENTHESIS') {
356✔
229
                        if (empty($token['parenthesis_opener'])) {
24✔
230
                                return null;
×
231
                        }
232
                        $startOfArguments = $token['parenthesis_opener'];
24✔
233
                } else {
6✔
234
                        if (empty($token['nested_parenthesis'])) {
356✔
235
                                return null;
352✔
236
                        }
237
                        $startingParenthesis = array_keys($token['nested_parenthesis']);
284✔
238
                        $startOfArguments = end($startingParenthesis);
284✔
239
                }
240

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

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

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

264
        /**
265
         * @param File $phpcsFile
266
         * @param int  $stackPtr
267
         *
268
         * @return bool
269
         */
270
        public static function isTokenInsideFunctionUseImport(File $phpcsFile, $stackPtr)
352✔
271
        {
272
                return is_int(self::getUseIndexForUseImport($phpcsFile, $stackPtr));
352✔
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)
352✔
284
        {
285
                $tokens = $phpcsFile->getTokens();
352✔
286

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

297
                $usePtr = self::getIntOrNull($phpcsFile->findPrevious(array_values($nonUseTokenTypes), $openParenPtr - 1, null, true, null, true));
224✔
298
                if (! is_int($usePtr) || $tokens[$usePtr]['code'] !== T_USE) {
224✔
299
                        return null;
212✔
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)
332✔
315
        {
316
                $tokens = $phpcsFile->getTokens();
332✔
317

318
                $openPtr = self::findContainingOpeningBracket($phpcsFile, $stackPtr);
332✔
319
                if (is_int($openPtr)) {
332✔
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);
216✔
322
                        if (is_int($functionPtr)) {
216✔
323
                                $functionTokenCode = $tokens[$functionPtr]['code'];
216✔
324
                                // In PHPCS 4.x, function names can be T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, or T_NAME_RELATIVE
325
                                $validFunctionTokens = [T_STRING];
216✔
326
                                if (defined('T_NAME_FULLY_QUALIFIED')) {
216✔
327
                                        $validFunctionTokens[] = T_NAME_FULLY_QUALIFIED;
108✔
328
                                }
329
                                if (defined('T_NAME_QUALIFIED')) {
216✔
330
                                        $validFunctionTokens[] = T_NAME_QUALIFIED;
108✔
331
                                }
332
                                if (defined('T_NAME_RELATIVE')) {
216✔
333
                                        $validFunctionTokens[] = T_NAME_RELATIVE;
108✔
334
                                }
335
                                if (in_array($functionTokenCode, $validFunctionTokens, true)) {
216✔
336
                                        return $functionPtr;
162✔
337
                                }
338
                        }
36✔
339
                }
36✔
340
                return null;
318✔
341
        }
342

343
        /**
344
         * @param File $phpcsFile
345
         * @param int  $stackPtr
346
         *
347
         * @return array<int, array<int>>
348
         */
349
        public static function findFunctionCallArguments(File $phpcsFile, $stackPtr)
36✔
350
        {
351
                $tokens = $phpcsFile->getTokens();
36✔
352

353
                // Slight hack: also allow this to find args for array constructor.
354
                if (($tokens[$stackPtr]['code'] !== T_STRING) && ($tokens[$stackPtr]['code'] !== T_ARRAY)) {
36✔
355
                        // Assume $stackPtr is something within the brackets, find our function call
356
                        $stackPtr = self::findFunctionCall($phpcsFile, $stackPtr);
24✔
357
                        if ($stackPtr === null) {
24✔
UNCOV
358
                                return [];
×
359
                        }
360
                }
6✔
361

362
                // $stackPtr is the function name, find our brackets after it
363
                $openPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
36✔
364
                if (($openPtr === false) || ($tokens[$openPtr]['code'] !== T_OPEN_PARENTHESIS)) {
36✔
365
                        return [];
×
366
                }
367

368
                if (!isset($tokens[$openPtr]['parenthesis_closer'])) {
36✔
UNCOV
369
                        return [];
×
370
                }
371
                $closePtr = $tokens[$openPtr]['parenthesis_closer'];
36✔
372

373
                $argPtrs = [];
36✔
374
                $lastPtr = $openPtr;
36✔
375
                $lastArgComma = $openPtr;
36✔
376
                $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
36✔
377
                while (is_int($nextPtr)) {
36✔
378
                        if (self::findContainingOpeningBracket($phpcsFile, $nextPtr) === $openPtr) {
36✔
379
                                // Comma is at our level of brackets, it's an argument delimiter.
380
                                $range = range($lastArgComma + 1, $nextPtr - 1);
36✔
381
                                array_push($argPtrs, $range);
36✔
382
                                $lastArgComma = $nextPtr;
36✔
383
                        }
9✔
384
                        $lastPtr = $nextPtr;
36✔
385
                        $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
36✔
386
                }
9✔
387
                $range = range($lastArgComma + 1, $closePtr - 1);
36✔
388
                $range = array_filter($range, function ($element) {
18✔
389
                        return is_int($element);
36✔
390
                });
36✔
391
                array_push($argPtrs, $range);
36✔
392

393
                return $argPtrs;
36✔
394
        }
395

396
        /**
397
         * @param File $phpcsFile
398
         * @param int  $stackPtr
399
         *
400
         * @return ?int
401
         */
402
        public static function getNextAssignPointer(File $phpcsFile, $stackPtr)
344✔
403
        {
404
                $tokens = $phpcsFile->getTokens();
344✔
405

406
                // Is the next non-whitespace an assignment?
407
                $nextPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
344✔
408
                if (
409
                        is_int($nextPtr)
344✔
410
                        && isset(Tokens::$assignmentTokens[$tokens[$nextPtr]['code']])
344✔
411
                        // Ignore double arrow to prevent triggering on `foreach ( $array as $k => $v )`.
412
                        && $tokens[$nextPtr]['code'] !== T_DOUBLE_ARROW
344✔
413
                ) {
86✔
414
                        return $nextPtr;
324✔
415
                }
416
                return null;
336✔
417
        }
418

419
        /**
420
         * @param string $varName
421
         *
422
         * @return string
423
         */
424
        public static function normalizeVarName($varName)
356✔
425
        {
426
                $result = preg_replace('/[{}$]/', '', $varName);
356✔
427
                return $result ? $result : $varName;
356✔
428
        }
429

430
        /**
431
         * @param File   $phpcsFile
432
         * @param int    $stackPtr
433
         * @param string $varName   (optional) if it differs from the normalized 'content' of the token at $stackPtr
434
         *
435
         * @return ?int
436
         */
437
        public static function findVariableScope(File $phpcsFile, $stackPtr, $varName = null)
356✔
438
        {
439
                $tokens = $phpcsFile->getTokens();
356✔
440
                $token = $tokens[$stackPtr];
356✔
441
                $varName = isset($varName) ? $varName : self::normalizeVarName($token['content']);
356✔
442

443
                $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
356✔
444

445
                if (!is_null($enclosingScopeIndex)) {
356✔
446
                        $arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
352✔
447
                        $isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex);
352✔
448
                        if ($isTokenInsideArrowFunctionBody) {
352✔
449
                                // Get the list of variables defined by the arrow function
450
                                // If this matches any of them, the scope is the arrow function,
451
                                // otherwise, it uses the enclosing scope.
452
                                if ($arrowFunctionIndex) {
36✔
453
                                        $variableNames = self::getVariablesDefinedByArrowFunction($phpcsFile, $arrowFunctionIndex);
36✔
454
                                        self::debug('findVariableScope: looking for', $varName, 'in arrow function variables', $variableNames);
36✔
455
                                        if (in_array($varName, $variableNames, true)) {
36✔
456
                                                return $arrowFunctionIndex;
36✔
457
                                        }
458
                                }
7✔
459
                        }
7✔
460
                }
88✔
461

462
                return $enclosingScopeIndex;
356✔
463
        }
464

465
        /**
466
         * Return the variable names and positions of each variable targetted by a `compact()` call.
467
         *
468
         * @param File                   $phpcsFile
469
         * @param int                    $stackPtr
470
         * @param array<int, array<int>> $arguments The stack pointers of each argument; see findFunctionCallArguments
471
         *
472
         * @return array<VariableInfo> each variable's firstRead position and its name; other VariableInfo properties are not set!
473
         */
474
        public static function getVariablesInsideCompact(File $phpcsFile, $stackPtr, $arguments)
12✔
475
        {
476
                $tokens = $phpcsFile->getTokens();
12✔
477
                $variablePositionsAndNames = [];
12✔
478

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

527
        /**
528
         * Return the token index of the scope start for a token
529
         *
530
         * For a variable within a function body, or a variable within a function
531
         * definition argument list, this will return the function keyword's index.
532
         *
533
         * For a variable within a "use" import list within a function definition,
534
         * this will return the enclosing scope, not the function keyword. This is
535
         * important to note because the "use" keyword performs double-duty, defining
536
         * variables for the function's scope, and consuming the variables in the
537
         * enclosing scope. Use `getUseIndexForUseImport` to determine if this
538
         * token needs to be treated as a "use".
539
         *
540
         * For a variable within an arrow function definition argument list,
541
         * this will return the arrow function's keyword index.
542
         *
543
         * For a variable in an arrow function body, this will return the enclosing
544
         * function's index, which may be incorrect.
545
         *
546
         * Since a variable in an arrow function's body may be imported from the
547
         * enclosing scope, it's important to test to see if the variable is in an
548
         * arrow function and also check its enclosing scope separately.
549
         *
550
         * @param File $phpcsFile
551
         * @param int  $stackPtr
552
         *
553
         * @return ?int
554
         */
555
        public static function findVariableScopeExceptArrowFunctions(File $phpcsFile, $stackPtr)
356✔
556
        {
557
                $tokens = $phpcsFile->getTokens();
356✔
558
                $allowedTypes = [
178✔
559
                        T_VARIABLE,
356✔
560
                        T_DOUBLE_QUOTED_STRING,
356✔
561
                        T_HEREDOC,
356✔
562
                        T_STRING,
356✔
563
                ];
267✔
564
                // In PHPCS 4.x, function names can be T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, or T_NAME_RELATIVE
565
                if (defined('T_NAME_FULLY_QUALIFIED')) {
356✔
566
                        $allowedTypes[] = T_NAME_FULLY_QUALIFIED;
178✔
567
                }
568
                if (defined('T_NAME_QUALIFIED')) {
356✔
569
                        $allowedTypes[] = T_NAME_QUALIFIED;
178✔
570
                }
571
                if (defined('T_NAME_RELATIVE')) {
356✔
572
                        $allowedTypes[] = T_NAME_RELATIVE;
178✔
573
                }
574
                if (! in_array($tokens[$stackPtr]['code'], $allowedTypes, true)) {
356✔
UNCOV
575
                        throw new \Exception("Cannot find variable scope for non-variable {$tokens[$stackPtr]['type']}");
×
576
                }
577

578
                $startOfTokenScope = self::getStartOfTokenScope($phpcsFile, $stackPtr);
356✔
579
                if (is_int($startOfTokenScope) && $startOfTokenScope > 0) {
356✔
580
                        return $startOfTokenScope;
336✔
581
                }
582

583
                // If there is no "conditions" array, this is a function definition argument.
584
                if (self::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
292✔
585
                        $functionPtr = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
232✔
586
                        if (! is_int($functionPtr)) {
232✔
UNCOV
587
                                throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
588
                        }
589
                        return $functionPtr;
232✔
590
                }
591

592
                self::debug('Cannot find function scope for variable at', $stackPtr);
124✔
593
                return $startOfTokenScope;
124✔
594
        }
595

596
        /**
597
         * Return the token index of the scope start for a variable token
598
         *
599
         * This will only work for a variable within a function's body. Otherwise,
600
         * see `findVariableScope`, which is more complex.
601
         *
602
         * Note that if used on a variable in an arrow function, it will return the
603
         * enclosing function's scope, which may be incorrect.
604
         *
605
         * @param File $phpcsFile
606
         * @param int  $stackPtr
607
         *
608
         * @return ?int
609
         */
610
        private static function getStartOfTokenScope(File $phpcsFile, $stackPtr)
356✔
611
        {
612
                $tokens = $phpcsFile->getTokens();
356✔
613
                $token = $tokens[$stackPtr];
356✔
614

615
                $inClass = false;
356✔
616
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
356✔
617
                $functionTokenTypes = [
178✔
618
                        T_FUNCTION,
356✔
619
                        T_CLOSURE,
356✔
620
                ];
267✔
621
                foreach (array_reverse($conditions, true) as $scopePtr => $scopeCode) {
356✔
622
                        if (in_array($scopeCode, $functionTokenTypes, true) || self::isArrowFunction($phpcsFile, $scopePtr)) {
340✔
623
                                return $scopePtr;
336✔
624
                        }
625
                        if (isset(Tokens::$ooScopeTokens[$scopeCode]) === true) {
164✔
626
                                $inClass = true;
68✔
627
                        }
17✔
628
                }
82✔
629

630
                if ($inClass) {
292✔
631
                        // If this is inside a class and not inside a function, this is either a
632
                        // class member variable definition, or a function argument. If it is a
633
                        // variable definition, it has no scope on its own (it can only be used
634
                        // with an object reference). If it is a function argument, we need to do
635
                        // more work (see `findVariableScopeExceptArrowFunctions`).
636
                        return null;
60✔
637
                }
638

639
                // If we can't find a scope, let's use the first token of the file.
640
                return 0;
252✔
641
        }
642

643
        /**
644
         * @param File $phpcsFile
645
         * @param int  $stackPtr
646
         *
647
         * @return bool
648
         */
UNCOV
649
        public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $stackPtr)
×
650
        {
UNCOV
651
                $tokens = $phpcsFile->getTokens();
×
UNCOV
652
                $token = $tokens[$stackPtr];
×
UNCOV
653
                $openParenIndices = isset($token['nested_parenthesis']) ? $token['nested_parenthesis'] : [];
×
UNCOV
654
                if (empty($openParenIndices)) {
×
UNCOV
655
                        return false;
×
656
                }
UNCOV
657
                $openParenPtr = $openParenIndices[0];
×
UNCOV
658
                return self::isArrowFunction($phpcsFile, $openParenPtr - 1);
×
659
        }
660

661
        /**
662
         * @param File $phpcsFile
663
         * @param int  $stackPtr
664
         * @param int  $enclosingScopeIndex
665
         *
666
         * @return ?int
667
         */
668
        public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
352✔
669
        {
670
                $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
352✔
671
                if (! is_int($arrowFunctionIndex)) {
352✔
672
                        return null;
352✔
673
                }
674
                $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex);
36✔
675
                if (! $arrowFunctionInfo) {
36✔
UNCOV
676
                        return null;
×
677
                }
678

679
                // We found the closest arrow function before this token. If the token is
680
                // within the scope of that arrow function, then return it.
681
                if ($stackPtr > $arrowFunctionInfo['scope_opener'] && $stackPtr < $arrowFunctionInfo['scope_closer']) {
36✔
682
                        return $arrowFunctionIndex;
36✔
683
                }
684

685
                // If the token is after the scope of the closest arrow function, we may
686
                // still be inside the scope of a nested arrow function, so we need to
687
                // search further back until we are certain there are no more arrow
688
                // functions.
689
                if ($stackPtr > $arrowFunctionInfo['scope_closer']) {
36✔
690
                        return self::getContainingArrowFunctionIndex($phpcsFile, $arrowFunctionIndex, $enclosingScopeIndex);
36✔
691
                }
692

UNCOV
693
                return null;
×
694
        }
695

696
        /**
697
         * Move back from the stackPtr to the start of the enclosing scope until we
698
         * find a 'fn' token that starts an arrow function, returning the index of
699
         * that token. Returns null if there are no arrow functions before stackPtr.
700
         *
701
         * Note that this does not guarantee that stackPtr is inside the arrow
702
         * function scope we find!
703
         *
704
         * @param File $phpcsFile
705
         * @param int  $stackPtr
706
         * @param int  $enclosingScopeIndex
707
         *
708
         * @return ?int
709
         */
710
        private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
352✔
711
        {
712
                $tokens = $phpcsFile->getTokens();
352✔
713
                for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) {
352✔
714
                        $token = $tokens[$index];
352✔
715
                        if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) {
352✔
716
                                return $index;
36✔
717
                        }
718
                }
88✔
719
                return null;
352✔
720
        }
721

722
        /**
723
         * @param File $phpcsFile
724
         * @param int  $stackPtr
725
         *
726
         * @return bool
727
         */
728
        public static function isArrowFunction(File $phpcsFile, $stackPtr)
356✔
729
        {
730
                $tokens = $phpcsFile->getTokens();
356✔
731
                if (defined('T_FN') && $tokens[$stackPtr]['code'] === T_FN) {
356✔
732
                        return true;
36✔
733
                }
734
                if ($tokens[$stackPtr]['content'] !== 'fn') {
356✔
735
                        return false;
356✔
736
                }
737
                // Make sure next non-space token is an open parenthesis
UNCOV
738
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
×
UNCOV
739
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
×
UNCOV
740
                        return false;
×
741
                }
742
                // Find the associated close parenthesis
UNCOV
743
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
×
744
                // Make sure the next token is a fat arrow
745
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
×
UNCOV
746
                if (! is_int($fatArrowIndex)) {
×
UNCOV
747
                        return false;
×
748
                }
UNCOV
749
                if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') {
×
750
                        return false;
×
751
                }
UNCOV
752
                return true;
×
753
        }
754

755
        /**
756
         * Find the opening and closing scope positions for an arrow function if the
757
         * given position is the start of the arrow function (the `fn` keyword
758
         * token).
759
         *
760
         * Returns null if the passed token is not an arrow function keyword.
761
         *
762
         * If the token is an arrow function keyword, the scope opener is returned as
763
         * the provided position.
764
         *
765
         * @param File $phpcsFile
766
         * @param int  $stackPtr
767
         *
768
         * @return ?array<string, int>
769
         */
770
        public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
36✔
771
        {
772
                $tokens = $phpcsFile->getTokens();
36✔
773
                if ($tokens[$stackPtr]['content'] !== 'fn') {
36✔
UNCOV
774
                        return null;
×
775
                }
776
                // Make sure next non-space token is an open parenthesis
777
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
36✔
778
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
36✔
UNCOV
779
                        return null;
×
780
                }
781
                // Find the associated close parenthesis
782
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
36✔
783
                // Make sure the next token is a fat arrow or a return type
784
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
36✔
785
                if (! is_int($fatArrowIndex)) {
36✔
UNCOV
786
                        return null;
×
787
                }
788
                if (
789
                        $tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW &&
36✔
790
                        $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW' &&
36✔
791
                        $tokens[$fatArrowIndex]['code'] !== T_COLON
29✔
792
                ) {
9✔
UNCOV
793
                        return null;
×
794
                }
795

796
                // Find the scope closer
797
                $scopeCloserIndex = null;
36✔
798
                $foundCurlyPairs = 0;
36✔
799
                $foundArrayPairs = 0;
36✔
800
                $foundParenPairs = 0;
36✔
801
                $arrowBodyStart = $tokens[$stackPtr]['parenthesis_closer'] + 1;
36✔
802
                $lastToken = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
36✔
803
                for ($index = $arrowBodyStart; $index < $lastToken; $index++) {
36✔
804
                        $token = $tokens[$index];
36✔
805
                        if (empty($token['code'])) {
36✔
806
                                $scopeCloserIndex = $index;
×
807
                                break;
×
808
                        }
809

810
                        $code = $token['code'];
36✔
811

812
                        // A semicolon is always a closer.
813
                        if ($code === T_SEMICOLON) {
36✔
814
                                $scopeCloserIndex = $index;
28✔
815
                                break;
28✔
816
                        }
817

818
                        // Track pair opening tokens.
819
                        if ($code === T_OPEN_CURLY_BRACKET) {
36✔
820
                                $foundCurlyPairs += 1;
8✔
821
                                continue;
8✔
822
                        }
823
                        if ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) {
36✔
824
                                $foundArrayPairs += 1;
28✔
825
                                continue;
28✔
826
                        }
827
                        if ($code === T_OPEN_PARENTHESIS) {
36✔
828
                                $foundParenPairs += 1;
28✔
829
                                continue;
28✔
830
                        }
831

832
                        // A pair closing is only an arrow func closer if there was no matching opening token.
833
                        if ($code === T_CLOSE_CURLY_BRACKET) {
36✔
UNCOV
834
                                if ($foundCurlyPairs === 0) {
×
UNCOV
835
                                        $scopeCloserIndex = $index;
×
UNCOV
836
                                        break;
×
837
                                }
UNCOV
838
                                $foundCurlyPairs -= 1;
×
UNCOV
839
                                continue;
×
840
                        }
841
                        if ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) {
36✔
842
                                if ($foundArrayPairs === 0) {
28✔
UNCOV
843
                                        $scopeCloserIndex = $index;
×
UNCOV
844
                                        break;
×
845
                                }
846
                                $foundArrayPairs -= 1;
28✔
847
                                continue;
28✔
848
                        }
849
                        if ($code === T_CLOSE_PARENTHESIS) {
36✔
850
                                if ($foundParenPairs === 0) {
28✔
851
                                        $scopeCloserIndex = $index;
8✔
852
                                        break;
8✔
853
                                }
854
                                $foundParenPairs -= 1;
28✔
855
                                continue;
28✔
856
                        }
857

858
                        // A comma is a closer only if we are not inside an opening token.
859
                        if ($code === T_COMMA) {
36✔
860
                                if (empty($foundArrayPairs) && empty($foundParenPairs) && empty($foundCurlyPairs)) {
28✔
861
                                        $scopeCloserIndex = $index;
16✔
862
                                        break;
16✔
863
                                }
864
                                continue;
20✔
865
                        }
866
                }
9✔
867

868
                if (! is_int($scopeCloserIndex)) {
36✔
UNCOV
869
                        return null;
×
870
                }
871

872
                return [
18✔
873
                        'scope_opener' => $stackPtr,
36✔
874
                        'scope_closer' => $scopeCloserIndex,
36✔
875
                ];
27✔
876
        }
877

878
        /**
879
         * Determine if a token is a list opener for list assignment/destructuring.
880
         *
881
         * The index provided can be either the opening square brace of a short list
882
         * assignment like the first character of `[$a] = $b;` or the `list` token of
883
         * an expression like `list($a) = $b;` or the opening parenthesis of that
884
         * expression.
885
         *
886
         * @param File $phpcsFile
887
         * @param int  $listOpenerIndex
888
         *
889
         * @return bool
890
         */
891
        private static function isListAssignment(File $phpcsFile, $listOpenerIndex)
32✔
892
        {
893
                $tokens = $phpcsFile->getTokens();
32✔
894
                // Match `[$a] = $b;` except for when the previous token is a parenthesis.
895
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SHORT_ARRAY) {
32✔
896
                        return true;
28✔
897
                }
898
                // Match `list($a) = $b;`
899
                if ($tokens[$listOpenerIndex]['code'] === T_LIST) {
32✔
900
                        return true;
32✔
901
                }
902

903
                // If $listOpenerIndex is the open parenthesis of `list($a) = $b;`, then
904
                // match that too.
905
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_PARENTHESIS) {
16✔
906
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
4✔
907
                        if (
908
                                isset($tokens[$previousTokenPtr])
4✔
909
                                && $tokens[$previousTokenPtr]['code'] === T_LIST
4✔
910
                        ) {
1✔
911
                                return true;
4✔
912
                        }
UNCOV
913
                        return true;
×
914
                }
915

916
                // If the list opener token is a square bracket that is preceeded by a
917
                // close parenthesis that has an owner which is a scope opener, then this
918
                // is a list assignment and not an array access.
919
                //
920
                // Match `if (true) [$a] = $b;`
921
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SQUARE_BRACKET) {
12✔
922
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
12✔
923
                        if (
924
                                isset($tokens[$previousTokenPtr])
12✔
925
                                && $tokens[$previousTokenPtr]['code'] === T_CLOSE_PARENTHESIS
12✔
926
                                && isset($tokens[$previousTokenPtr]['parenthesis_owner'])
12✔
927
                                && isset(Tokens::$scopeOpeners[$tokens[$tokens[$previousTokenPtr]['parenthesis_owner']]['code']])
12✔
928
                        ) {
4✔
929
                                return true;
4✔
930
                        }
931
                }
2✔
932

933
                return false;
8✔
934
        }
935

936
        /**
937
         * Return a list of indices for variables assigned within a list assignment.
938
         *
939
         * The index provided can be either the opening square brace of a short list
940
         * assignment like the first character of `[$a] = $b;` or the `list` token of
941
         * an expression like `list($a) = $b;` or the opening parenthesis of that
942
         * expression.
943
         *
944
         * @param File $phpcsFile
945
         * @param int  $listOpenerIndex
946
         *
947
         * @return ?array<int>
948
         */
949
        public static function getListAssignments(File $phpcsFile, $listOpenerIndex)
48✔
950
        {
951
                $tokens = $phpcsFile->getTokens();
48✔
952
                self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]);
48✔
953

954
                // First find the end of the list
955
                $closePtr = null;
48✔
956
                if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) {
48✔
957
                        $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer'];
48✔
958
                }
12✔
959
                if (isset($tokens[$listOpenerIndex]['bracket_closer'])) {
48✔
960
                        $closePtr = $tokens[$listOpenerIndex]['bracket_closer'];
48✔
961
                }
12✔
962
                if (! $closePtr) {
48✔
UNCOV
963
                        return null;
×
964
                }
965

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

969
                // If the next token isn't an assignment, check for nested brackets because we might be a nested assignment
970
                if (! is_int($assignPtr) || $tokens[$assignPtr]['code'] !== T_EQUAL) {
48✔
971
                        // Collect the enclosing list open/close tokens ($parents is an assoc array keyed by opener index and the value is the closer index)
972
                        $parents = isset($tokens[$listOpenerIndex]['nested_parenthesis']) ? $tokens[$listOpenerIndex]['nested_parenthesis'] : [];
40✔
973
                        // There's no record of nested brackets for short lists; we'll have to find the parent ourselves
974
                        if (empty($parents)) {
40✔
975
                                $parentSquareBracketPtr = self::findContainingOpeningSquareBracket($phpcsFile, $listOpenerIndex);
40✔
976
                                if (is_int($parentSquareBracketPtr)) {
40✔
977
                                        // Make sure that the parent is really a parent by checking that its
978
                                        // closing index is outside of the current bracket's closing index.
979
                                        $parentSquareBracketToken = $tokens[$parentSquareBracketPtr];
4✔
980
                                        $parentSquareBracketClosePtr = $parentSquareBracketToken['bracket_closer'];
4✔
981
                                        if ($parentSquareBracketClosePtr && $parentSquareBracketClosePtr > $closePtr) {
4✔
982
                                                self::debug("found enclosing bracket for {$listOpenerIndex}: {$parentSquareBracketPtr}");
4✔
983
                                                // Collect the opening index, but we don't actually need the closing paren index so just make that 0
984
                                                $parents = [$parentSquareBracketPtr => 0];
4✔
985
                                        }
1✔
986
                                }
1✔
987
                        }
10✔
988
                        // If we have no parents, this is not a nested assignment and therefore is not an assignment
989
                        if (empty($parents)) {
40✔
990
                                return null;
40✔
991
                        }
992

993
                        // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion)
994
                        $isNestedAssignment = null;
28✔
995
                        $parentListOpener = array_keys(array_reverse($parents, true))[0];
28✔
996
                        $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener);
28✔
997
                        if ($isNestedAssignment === null) {
28✔
998
                                return null;
28✔
999
                        }
1000
                }
1✔
1001

1002
                $variablePtrs = [];
32✔
1003

1004
                $currentPtr = $listOpenerIndex;
32✔
1005
                $variablePtr = 0;
32✔
1006
                while ($currentPtr < $closePtr && is_int($variablePtr)) {
32✔
1007
                        $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr);
32✔
1008
                        if (is_int($variablePtr)) {
32✔
1009
                                $variablePtrs[] = $variablePtr;
32✔
1010
                        }
8✔
1011
                        ++$currentPtr;
32✔
1012
                }
8✔
1013

1014
                if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) {
32✔
1015
                        return null;
8✔
1016
                }
1017

1018
                return $variablePtrs;
32✔
1019
        }
1020

1021
        /**
1022
         * @param File $phpcsFile
1023
         * @param int  $stackPtr
1024
         *
1025
         * @return string[]
1026
         */
1027
        public static function getVariablesDefinedByArrowFunction(File $phpcsFile, $stackPtr)
36✔
1028
        {
1029
                $tokens = $phpcsFile->getTokens();
36✔
1030
                $arrowFunctionToken = $tokens[$stackPtr];
36✔
1031
                $variableNames = [];
36✔
1032
                self::debug('looking for variables in arrow function token', $arrowFunctionToken);
36✔
1033
                for ($index = $arrowFunctionToken['parenthesis_opener']; $index < $arrowFunctionToken['parenthesis_closer']; $index++) {
36✔
1034
                        $token = $tokens[$index];
36✔
1035
                        if ($token['code'] === T_VARIABLE) {
36✔
1036
                                $variableNames[] = self::normalizeVarName($token['content']);
36✔
1037
                        }
9✔
1038
                }
9✔
1039
                self::debug('found these variables in arrow function token', $variableNames);
36✔
1040
                return $variableNames;
36✔
1041
        }
1042

1043
        /**
1044
         * @return void
1045
         */
1046
        public static function debug()
356✔
1047
        {
1048
                $messages = func_get_args();
356✔
1049
                if (! defined('PHP_CODESNIFFER_VERBOSITY')) {
356✔
UNCOV
1050
                        return;
×
1051
                }
1052
                if (PHP_CODESNIFFER_VERBOSITY <= 3) {
356✔
1053
                        return;
356✔
1054
                }
UNCOV
1055
                $output = PHP_EOL . 'VariableAnalysisSniff: DEBUG:';
×
UNCOV
1056
                foreach ($messages as $message) {
×
UNCOV
1057
                        if (is_string($message) || is_numeric($message)) {
×
UNCOV
1058
                                $output .= ' "' . $message . '"';
×
UNCOV
1059
                                continue;
×
1060
                        }
UNCOV
1061
                        $output .= PHP_EOL . var_export($message, true) . PHP_EOL;
×
1062
                }
UNCOV
1063
                $output .= PHP_EOL;
×
UNCOV
1064
                echo $output;
×
1065
        }
1066

1067
        /**
1068
         * @param string $pattern
1069
         * @param string $value
1070
         *
1071
         * @return string[]
1072
         */
1073
        public static function splitStringToArray($pattern, $value)
28✔
1074
        {
1075
                if (empty($pattern)) {
28✔
UNCOV
1076
                        return [];
×
1077
                }
1078
                $result = preg_split($pattern, $value);
28✔
1079
                return is_array($result) ? $result : [];
28✔
1080
        }
1081

1082
        /**
1083
         * @param string $varName
1084
         *
1085
         * @return bool
1086
         */
1087
        public static function isVariableANumericVariable($varName)
340✔
1088
        {
1089
                return is_numeric(substr($varName, 0, 1));
340✔
1090
        }
1091

1092
        /**
1093
         * @param File $phpcsFile
1094
         * @param int  $stackPtr
1095
         *
1096
         * @return bool
1097
         */
1098
        public static function isVariableInsideElseCondition(File $phpcsFile, $stackPtr)
332✔
1099
        {
1100
                $tokens = $phpcsFile->getTokens();
332✔
1101
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
332✔
1102
                $nonFunctionTokenTypes[] = T_OPEN_PARENTHESIS;
332✔
1103
                $nonFunctionTokenTypes[] = T_INLINE_HTML;
332✔
1104
                $nonFunctionTokenTypes[] = T_CLOSE_TAG;
332✔
1105
                $nonFunctionTokenTypes[] = T_VARIABLE;
332✔
1106
                $nonFunctionTokenTypes[] = T_ELLIPSIS;
332✔
1107
                $nonFunctionTokenTypes[] = T_COMMA;
332✔
1108
                $nonFunctionTokenTypes[] = T_STRING;
332✔
1109
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
332✔
1110
                $elsePtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $stackPtr - 1, null, true, null, true));
332✔
1111
                $elseTokenTypes = [
166✔
1112
                        T_ELSE,
332✔
1113
                        T_ELSEIF,
332✔
1114
                ];
249✔
1115
                if (is_int($elsePtr) && in_array($tokens[$elsePtr]['code'], $elseTokenTypes, true)) {
332✔
1116
                        return true;
16✔
1117
                }
1118
                return false;
332✔
1119
        }
1120

1121
        /**
1122
         * @param File $phpcsFile
1123
         * @param int  $stackPtr
1124
         *
1125
         * @return bool
1126
         */
1127
        public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr)
332✔
1128
        {
1129
                $tokens = $phpcsFile->getTokens();
332✔
1130
                $token = $tokens[$stackPtr];
332✔
1131
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
332✔
1132
                $elseTokenTypes = [
166✔
1133
                        T_ELSE,
332✔
1134
                        T_ELSEIF,
332✔
1135
                ];
249✔
1136
                foreach (array_reverse($conditions, true) as $scopeCode) {
332✔
1137
                        if (in_array($scopeCode, $elseTokenTypes, true)) {
312✔
1138
                                return true;
16✔
1139
                        }
1140
                }
83✔
1141

1142
                // Some else body code will not have conditions because it is inline (no
1143
                // curly braces) so we have to look in other ways.
1144
                $previousSemicolonPtr = $phpcsFile->findPrevious([T_SEMICOLON], $stackPtr - 1);
332✔
1145
                if (! is_int($previousSemicolonPtr)) {
332✔
1146
                        $previousSemicolonPtr = 0;
152✔
1147
                }
38✔
1148
                $elsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1, $previousSemicolonPtr);
332✔
1149
                if (is_int($elsePtr)) {
332✔
1150
                        return true;
8✔
1151
                }
1152

1153
                return false;
332✔
1154
        }
1155

1156
        /**
1157
         * @param File $phpcsFile
1158
         * @param int  $stackPtr
1159
         *
1160
         * @return int[]
1161
         */
1162
        public static function getAttachedBlockIndicesForElse(File $phpcsFile, $stackPtr)
24✔
1163
        {
1164
                $currentElsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1);
24✔
1165
                if (! is_int($currentElsePtr)) {
24✔
UNCOV
1166
                        throw new \Exception("Cannot find expected else at {$stackPtr}");
×
1167
                }
1168

1169
                $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
24✔
1170
                if (! is_int($ifPtr)) {
24✔
UNCOV
1171
                        throw new \Exception("Cannot find if for else at {$stackPtr}");
×
1172
                }
1173
                $blockIndices = [$ifPtr];
24✔
1174

1175
                $previousElseIfPtr = $currentElsePtr;
24✔
1176
                do {
1177
                        $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
24✔
1178
                        if (is_int($elseIfPtr)) {
24✔
1179
                                $blockIndices[] = $elseIfPtr;
16✔
1180
                                $previousElseIfPtr = $elseIfPtr;
16✔
1181
                        }
4✔
1182
                } while (is_int($elseIfPtr));
24✔
1183

1184
                return $blockIndices;
24✔
1185
        }
1186

1187
        /**
1188
         * @param int $needle
1189
         * @param int $scopeStart
1190
         * @param int $scopeEnd
1191
         *
1192
         * @return bool
1193
         */
1194
        public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
24✔
1195
        {
1196
                return ($needle > $scopeStart && $needle < $scopeEnd);
24✔
1197
        }
1198

1199
        /**
1200
         * @param File $phpcsFile
1201
         * @param int  $scopeStartIndex
1202
         *
1203
         * @return int
1204
         */
1205
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
356✔
1206
        {
1207
                $tokens = $phpcsFile->getTokens();
356✔
1208
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
356✔
1209

1210
                if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) {
356✔
1211
                        $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex);
36✔
1212
                        $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex;
36✔
1213
                }
9✔
1214

1215
                if ($scopeStartIndex === 0) {
356✔
1216
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
356✔
1217
                }
89✔
1218
                return $scopeCloserIndex;
356✔
1219
        }
1220

1221
        /**
1222
         * @param File $phpcsFile
1223
         *
1224
         * @return int
1225
         */
1226
        public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
356✔
1227
        {
1228
                $tokens = $phpcsFile->getTokens();
356✔
1229
                foreach (array_reverse($tokens, true) as $index => $token) {
356✔
1230
                        if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
356✔
1231
                                return $index;
356✔
1232
                        }
1233
                }
87✔
UNCOV
1234
                self::debug('no non-empty token found for end of file');
×
UNCOV
1235
                return 0;
×
1236
        }
1237

1238
        /**
1239
         * @param VariableInfo $varInfo
1240
         * @param ScopeInfo    $scopeInfo
1241
         *
1242
         * @return bool
1243
         */
1244
        public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo)
68✔
1245
        {
1246
                $foundVarPosition = false;
68✔
1247
                foreach ($scopeInfo->variables as $variable) {
68✔
1248
                        if ($variable === $varInfo) {
68✔
1249
                                $foundVarPosition = true;
68✔
1250
                                continue;
68✔
1251
                        }
1252
                        if (! $foundVarPosition) {
44✔
1253
                                continue;
36✔
1254
                        }
1255
                        if ($variable->scopeType !== ScopeType::PARAM) {
44✔
1256
                                continue;
36✔
1257
                        }
1258
                        if ($variable->firstRead) {
16✔
1259
                                return true;
16✔
1260
                        }
1261
                }
17✔
1262
                return false;
68✔
1263
        }
1264

1265
        /**
1266
         * @param File         $phpcsFile
1267
         * @param VariableInfo $varInfo
1268
         * @param ScopeInfo    $scopeInfo
1269
         *
1270
         * @return bool
1271
         */
1272
        public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
4✔
1273
        {
1274
                $requireTokens = [
2✔
1275
                        T_REQUIRE,
4✔
1276
                        T_REQUIRE_ONCE,
4✔
1277
                        T_INCLUDE,
4✔
1278
                        T_INCLUDE_ONCE,
4✔
1279
                ];
3✔
1280
                $indexToStartSearch = $varInfo->firstDeclared;
4✔
1281
                if (! empty($varInfo->firstInitialized)) {
4✔
1282
                        $indexToStartSearch = $varInfo->firstInitialized;
4✔
1283
                }
1✔
1284
                $tokens = $phpcsFile->getTokens();
4✔
1285
                $indexToStopSearch = isset($tokens[$scopeInfo->scopeStartIndex]['scope_closer']) ? $tokens[$scopeInfo->scopeStartIndex]['scope_closer'] : null;
4✔
1286
                if (! is_int($indexToStartSearch) || ! is_int($indexToStopSearch)) {
4✔
UNCOV
1287
                        return false;
×
1288
                }
1289
                $requireTokenIndex = $phpcsFile->findNext($requireTokens, $indexToStartSearch + 1, $indexToStopSearch);
4✔
1290
                if (is_int($requireTokenIndex)) {
4✔
1291
                        return true;
4✔
1292
                }
UNCOV
1293
                return false;
×
1294
        }
1295

1296
        /**
1297
         * Find the index of the function keyword for a token in a function call's arguments
1298
         *
1299
         * For the variable `$foo` in the expression `doSomething($foo)`, this will
1300
         * return the index of the `doSomething` token.
1301
         *
1302
         * @param File $phpcsFile
1303
         * @param int  $stackPtr
1304
         *
1305
         * @return ?int
1306
         */
1307
        public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr)
344✔
1308
        {
1309
                $tokens = $phpcsFile->getTokens();
344✔
1310
                $token = $tokens[$stackPtr];
344✔
1311
                if (empty($token['nested_parenthesis'])) {
344✔
1312
                        return null;
336✔
1313
                }
1314
                /**
1315
                 * @var list<int|string>
1316
                 */
1317
                $startingParenthesis = array_keys($token['nested_parenthesis']);
216✔
1318
                $startOfArguments = end($startingParenthesis);
216✔
1319
                if (! is_int($startOfArguments)) {
216✔
UNCOV
1320
                        return null;
×
1321
                }
1322

1323
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
216✔
1324
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
216✔
1325
                if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
216✔
UNCOV
1326
                        return null;
×
1327
                }
1328
                if (
1329
                        $tokens[$functionPtr]['content'] === 'function'
216✔
1330
                        || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))
216✔
1331
                ) {
54✔
1332
                        // If there is a function/fn keyword before the beginning of the parens,
1333
                        // this is a function definition and not a function call.
UNCOV
1334
                        return null;
×
1335
                }
1336
                if (! empty($tokens[$functionPtr]['scope_opener'])) {
216✔
1337
                        // If the alleged function name has a scope, this is not a function call.
1338
                        return null;
142✔
1339
                }
1340

1341
                $functionNameType = $tokens[$functionPtr]['code'];
182✔
1342
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
182✔
1343
                        // If the alleged function name is not a variable or a string, this is
1344
                        // not a function call.
1345
                        return null;
48✔
1346
                }
1347

1348
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
178✔
1349
                        // If the variable is inside a different scope than the function name,
1350
                        // the function call doesn't apply to the variable.
1351
                        return null;
32✔
1352
                }
1353

1354
                return $functionPtr;
178✔
1355
        }
1356

1357
        /**
1358
         * @param File $phpcsFile
1359
         * @param int  $stackPtr
1360
         *
1361
         * @return bool
1362
         */
1363
        public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
332✔
1364
        {
1365
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
332✔
1366
                if (! is_int($functionIndex)) {
332✔
1367
                        return false;
314✔
1368
                }
1369
                $tokens = $phpcsFile->getTokens();
178✔
1370
                if (! isset($tokens[$functionIndex])) {
178✔
UNCOV
1371
                        return false;
×
1372
                }
1373
                $allowedFunctionNames = [
89✔
1374
                        'isset',
178✔
1375
                        'empty',
134✔
1376
                ];
134✔
1377
                if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) {
178✔
1378
                        return true;
20✔
1379
                }
1380
                return false;
170✔
1381
        }
1382

1383
        /**
1384
         * @param File $phpcsFile
1385
         * @param int  $stackPtr
1386
         *
1387
         * @return bool
1388
         */
1389
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
260✔
1390
        {
1391
                $tokens = $phpcsFile->getTokens();
260✔
1392
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
260✔
1393

1394
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
260✔
1395
                if (! is_int($arrayPushOperatorIndex1)) {
260✔
UNCOV
1396
                        return false;
×
1397
                }
1398
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
260✔
1399
                        return false;
260✔
1400
                }
1401

1402
                $arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true));
36✔
1403
                if (! is_int($arrayPushOperatorIndex2)) {
36✔
UNCOV
1404
                        return false;
×
1405
                }
1406
                if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') {
36✔
1407
                        return false;
8✔
1408
                }
1409

1410
                $arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true));
28✔
1411
                if (! is_int($arrayPushOperatorIndex3)) {
28✔
UNCOV
1412
                        return false;
×
1413
                }
1414
                if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') {
28✔
UNCOV
1415
                        return false;
×
1416
                }
1417

1418
                return true;
28✔
1419
        }
1420

1421
        /**
1422
         * @param File $phpcsFile
1423
         * @param int  $stackPtr
1424
         *
1425
         * @return bool
1426
         */
1427
        public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
260✔
1428
        {
1429
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
260✔
1430
                if (! is_int($functionIndex)) {
260✔
1431
                        return false;
230✔
1432
                }
1433
                $tokens = $phpcsFile->getTokens();
102✔
1434
                if (! isset($tokens[$functionIndex])) {
102✔
UNCOV
1435
                        return false;
×
1436
                }
1437
                if ($tokens[$functionIndex]['content'] === 'unset') {
102✔
1438
                        return true;
8✔
1439
                }
1440
                return false;
94✔
1441
        }
1442

1443
        /**
1444
         * @param File $phpcsFile
1445
         * @param int  $stackPtr
1446
         *
1447
         * @return bool
1448
         */
1449
        public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr)
324✔
1450
        {
1451
                $previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
324✔
1452
                if (! is_int($previousStatementPtr)) {
324✔
1453
                        $previousStatementPtr = 1;
40✔
1454
                }
10✔
1455
                $previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
324✔
1456
                if (is_int($previousTokenPtr)) {
324✔
1457
                        return true;
4✔
1458
                }
1459
                return false;
324✔
1460
        }
1461

1462
        /**
1463
         * @param File $phpcsFile
1464
         * @param int  $stackPtr
1465
         *
1466
         * @return bool
1467
         */
1468
        public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
344✔
1469
        {
1470
                // Is the next non-whitespace an assignment?
1471
                $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
344✔
1472
                if (! is_int($assignPtr)) {
344✔
1473
                        return false;
336✔
1474
                }
1475

1476
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1477
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
324✔
1478
                        self::debug('found variable variable');
4✔
1479
                        return false;
4✔
1480
                }
1481
                return true;
324✔
1482
        }
1483

1484
        /**
1485
         * @param File $phpcsFile
1486
         * @param int  $stackPtr
1487
         *
1488
         * @return bool
1489
         */
1490
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
324✔
1491
        {
1492
                $tokens = $phpcsFile->getTokens();
324✔
1493

1494
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
324✔
1495
                if ($prev === false) {
324✔
UNCOV
1496
                        return false;
×
1497
                }
1498
                if ($tokens[$prev]['code'] === T_DOLLAR) {
324✔
1499
                        return true;
4✔
1500
                }
1501
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
324✔
1502
                        return false;
268✔
1503
                }
1504

1505
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
232✔
1506
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
232✔
UNCOV
1507
                        return true;
×
1508
                }
1509
                return false;
232✔
1510
        }
1511

1512
        /**
1513
         * @param File $phpcsFile
1514
         * @param int  $stackPtr
1515
         *
1516
         * @return EnumInfo|null
1517
         */
1518
        public static function makeEnumInfo(File $phpcsFile, $stackPtr)
4✔
1519
        {
1520
                $tokens = $phpcsFile->getTokens();
4✔
1521
                $token = $tokens[$stackPtr];
4✔
1522

1523
                if (isset($token['scope_opener'])) {
4✔
1524
                        $blockStart = $token['scope_opener'];
2✔
1525
                        $blockEnd = $token['scope_closer'];
2✔
1526
                } else {
1527
                        // Enums before phpcs could detect them do not have scopes so we have to
1528
                        // find them ourselves.
1529

1530
                        $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
4✔
1531
                        if (! is_int($blockStart)) {
4✔
1532
                                return null;
4✔
1533
                        }
1534
                        $blockEnd = $tokens[$blockStart]['bracket_closer'];
2✔
1535
                }
1536

1537
                return new EnumInfo(
4✔
1538
                        $stackPtr,
4✔
1539
                        $blockStart,
4✔
1540
                        $blockEnd
3✔
1541
                );
3✔
1542
        }
1543

1544
        /**
1545
         * @param File $phpcsFile
1546
         * @param int  $stackPtr
1547
         *
1548
         * @return ForLoopInfo
1549
         */
1550
        public static function makeForLoopInfo(File $phpcsFile, $stackPtr)
8✔
1551
        {
1552
                $tokens = $phpcsFile->getTokens();
8✔
1553
                $token = $tokens[$stackPtr];
8✔
1554
                $forIndex = $stackPtr;
8✔
1555
                $blockStart = $token['parenthesis_closer'];
8✔
1556
                if (isset($token['scope_opener'])) {
8✔
1557
                        $blockStart = $token['scope_opener'];
8✔
1558
                        $blockEnd = $token['scope_closer'];
8✔
1559
                } else {
2✔
1560
                        // Some for loop blocks will not have scope positions because it they are
1561
                        // inline (no curly braces) so we have to find the end of their scope by
1562
                        // looking for the end of the next statement.
1563
                        $nextSemicolonIndex = $phpcsFile->findNext([T_SEMICOLON], $token['parenthesis_closer']);
8✔
1564
                        if (! is_int($nextSemicolonIndex)) {
8✔
1565
                                $nextSemicolonIndex = $token['parenthesis_closer'] + 1;
×
1566
                        }
1567
                        $blockEnd = $nextSemicolonIndex;
8✔
1568
                }
1569
                $initStart = intval($token['parenthesis_opener']) + 1;
8✔
1570
                $initEnd = null;
8✔
1571
                $conditionStart = null;
8✔
1572
                $conditionEnd = null;
8✔
1573
                $incrementStart = null;
8✔
1574
                $incrementEnd = $token['parenthesis_closer'] - 1;
8✔
1575

1576
                $semicolonCount = 0;
8✔
1577
                $forLoopLevel = $tokens[$forIndex]['level'];
8✔
1578
                $forLoopNestedParensCount = 1;
8✔
1579

1580
                if (isset($tokens[$forIndex]['nested_parenthesis'])) {
8✔
UNCOV
1581
                        $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
×
1582
                }
1583

1584
                for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
8✔
1585
                        if ($tokens[$i]['code'] !== T_SEMICOLON) {
8✔
1586
                                continue;
8✔
1587
                        }
1588

1589
                        if ($tokens[$i]['level'] !== $forLoopLevel) {
8✔
1590
                                continue;
8✔
1591
                        }
1592

1593
                        if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
8✔
UNCOV
1594
                                continue;
×
1595
                        }
1596

1597
                        switch ($semicolonCount) {
1598
                                case 0:
8✔
1599
                                        $initEnd = $i;
8✔
1600
                                        $conditionStart = $initEnd + 1;
8✔
1601
                                        break;
8✔
1602
                                case 1:
8✔
1603
                                        $conditionEnd = $i;
8✔
1604
                                        $incrementStart = $conditionEnd + 1;
8✔
1605
                                        break;
8✔
1606
                        }
1607
                        $semicolonCount += 1;
8✔
1608
                }
2✔
1609

1610
                if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
8✔
UNCOV
1611
                        throw new \Exception("Cannot parse for loop at position {$forIndex}");
×
1612
                }
1613

1614
                return new ForLoopInfo(
8✔
1615
                        $forIndex,
8✔
1616
                        $blockStart,
8✔
1617
                        $blockEnd,
8✔
1618
                        $initStart,
8✔
1619
                        $initEnd,
8✔
1620
                        $conditionStart,
8✔
1621
                        $conditionEnd,
8✔
1622
                        $incrementStart,
8✔
1623
                        $incrementEnd
6✔
1624
                );
6✔
1625
        }
1626

1627
        /**
1628
         * @param int                     $stackPtr
1629
         * @param array<int, ForLoopInfo> $forLoops
1630
         * @return ForLoopInfo|null
1631
         */
1632
        public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
352✔
1633
        {
1634
                foreach ($forLoops as $forLoop) {
352✔
1635
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1636
                                return $forLoop;
8✔
1637
                        }
1638
                }
88✔
1639
                return null;
352✔
1640
        }
1641

1642
        /**
1643
         * Return true if the token looks like constructor promotion.
1644
         *
1645
         * Call on a parameter variable token only.
1646
         *
1647
         * @param File $phpcsFile
1648
         * @param int  $stackPtr
1649
         *
1650
         * @return bool
1651
         */
1652
        public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
232✔
1653
        {
1654
                // If we are not in a function's parameters, this is not promotion.
1655
                $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
232✔
1656
                if (! $functionIndex) {
232✔
UNCOV
1657
                        return false;
×
1658
                }
1659

1660
                $tokens = $phpcsFile->getTokens();
232✔
1661

1662
                // Move backwards from the token, ignoring whitespace, typehints, and the
1663
                // 'readonly' keyword, and return true if the previous token is a
1664
                // visibility keyword (eg: `public`).
1665
                for ($i = $stackPtr - 1; $i > $functionIndex; $i--) {
232✔
1666
                        if (in_array($tokens[$i]['code'], Tokens::$scopeModifiers, true)) {
232✔
1667
                                return true;
12✔
1668
                        }
1669
                        if (in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) {
232✔
1670
                                continue;
160✔
1671
                        }
1672
                        if ($tokens[$i]['content'] === 'readonly') {
232✔
1673
                                continue;
8✔
1674
                        }
1675
                        if (self::isTokenPartOfTypehint($phpcsFile, $i)) {
232✔
1676
                                continue;
32✔
1677
                        }
1678
                        return false;
228✔
1679
                }
1680
                return false;
×
1681
        }
1682

1683
        /**
1684
         * If looking at a function call token, return a string for the full function
1685
         * name including any inline namespace.
1686
         *
1687
         * So for example, if the call looks like `\My\Namespace\doSomething($bar)`
1688
         * and `$stackPtr` refers to `doSomething`, this will return
1689
         * `\My\Namespace\doSomething`.
1690
         *
1691
         * @param File $phpcsFile
1692
         * @param int  $stackPtr
1693
         *
1694
         * @return string|null
1695
         */
1696
        public static function getFunctionNameWithNamespace(File $phpcsFile, $stackPtr)
158✔
1697
        {
1698
                $tokens = $phpcsFile->getTokens();
158✔
1699

1700
                if (! isset($tokens[$stackPtr])) {
158✔
UNCOV
1701
                        return null;
×
1702
                }
1703
                $startOfScope = self::findVariableScope($phpcsFile, $stackPtr);
158✔
1704
                $functionName = $tokens[$stackPtr]['content'];
158✔
1705

1706
                // In PHPCS 4.x, T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, and T_NAME_RELATIVE
1707
                // tokens already contain the full namespaced name, so we can return early.
1708
                if (defined('T_NAME_FULLY_QUALIFIED') && $tokens[$stackPtr]['code'] === T_NAME_FULLY_QUALIFIED) {
158✔
1709
                        return $functionName;
8✔
1710
                }
1711
                if (defined('T_NAME_QUALIFIED') && $tokens[$stackPtr]['code'] === T_NAME_QUALIFIED) {
158✔
NEW
UNCOV
1712
                        return $functionName;
×
1713
                }
1714
                if (defined('T_NAME_RELATIVE') && $tokens[$stackPtr]['code'] === T_NAME_RELATIVE) {
158✔
NEW
UNCOV
1715
                        return $functionName;
×
1716
                }
1717

1718
                // Move backwards from the token, collecting namespace separators and
1719
                // strings, until we encounter whitespace or something else.
1720
                $partOfNamespace = [T_NS_SEPARATOR, T_STRING];
158✔
1721
                if (defined('T_NAME_QUALIFIED')) {
158✔
1722
                        $partOfNamespace[] = T_NAME_QUALIFIED;
78✔
1723
                }
1724
                if (defined('T_NAME_RELATIVE')) {
158✔
1725
                        $partOfNamespace[] = T_NAME_RELATIVE;
78✔
1726
                }
1727
                if (defined('T_NAME_FULLY_QUALIFIED')) {
158✔
1728
                        $partOfNamespace[] = T_NAME_FULLY_QUALIFIED;
78✔
1729
                }
1730
                for ($i = $stackPtr - 1; $i > $startOfScope; $i--) {
158✔
1731
                        if (! in_array($tokens[$i]['code'], $partOfNamespace, true)) {
158✔
1732
                                break;
158✔
1733
                        }
1734
                        $functionName = "{$tokens[$i]['content']}{$functionName}";
8✔
1735
                }
4✔
1736
                return $functionName;
158✔
1737
        }
1738

1739
        /**
1740
         * Return false if the token is definitely not part of a typehint
1741
         *
1742
         * @param File $phpcsFile
1743
         * @param int  $stackPtr
1744
         *
1745
         * @return bool
1746
         */
1747
        private static function isTokenPossiblyPartOfTypehint(File $phpcsFile, $stackPtr)
232✔
1748
        {
1749
                $tokens = $phpcsFile->getTokens();
232✔
1750
                $token = $tokens[$stackPtr];
232✔
1751
                if ($token['code'] === 'PHPCS_T_NULLABLE') {
232✔
1752
                        return true;
8✔
1753
                }
1754
                if (defined('T_NAME_QUALIFIED') && $token['code'] === T_NAME_QUALIFIED) {
232✔
NEW
UNCOV
1755
                        return true;
×
1756
                }
1757
                if (defined('T_NAME_RELATIVE') && $token['code'] === T_NAME_RELATIVE) {
232✔
NEW
UNCOV
1758
                        return true;
×
1759
                }
1760
                if (defined('T_NAME_FULLY_QUALIFIED') && $token['code'] === T_NAME_FULLY_QUALIFIED) {
232✔
1761
                        return true;
12✔
1762
                }
1763
                if ($token['code'] === T_NS_SEPARATOR) {
232✔
1764
                        return true;
8✔
1765
                }
1766
                if ($token['code'] === T_STRING) {
232✔
1767
                        return true;
32✔
1768
                }
1769
                if ($token['code'] === T_TRUE) {
232✔
1770
                        return true;
8✔
1771
                }
1772
                if ($token['code'] === T_FALSE) {
232✔
1773
                        return true;
8✔
1774
                }
1775
                if ($token['code'] === T_NULL) {
232✔
1776
                        return true;
8✔
1777
                }
1778
                if ($token['content'] === '|') {
232✔
1779
                        return true;
16✔
1780
                }
1781
                if (in_array($token['code'], Tokens::$emptyTokens)) {
232✔
1782
                        return true;
32✔
1783
                }
1784
                return false;
232✔
1785
        }
1786

1787
        /**
1788
         * Return true if the token is inside a typehint
1789
         *
1790
         * @param File $phpcsFile
1791
         * @param int  $stackPtr
1792
         *
1793
         * @return bool
1794
         */
1795
        public static function isTokenPartOfTypehint(File $phpcsFile, $stackPtr)
232✔
1796
        {
1797
                $tokens = $phpcsFile->getTokens();
232✔
1798

1799
                if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $stackPtr)) {
232✔
1800
                        return false;
228✔
1801
                }
1802

1803
                // Examine every following token, ignoring everything that might be part of
1804
                // a typehint. If we find a variable at the end, this is part of a
1805
                // typehint.
1806
                $i = $stackPtr;
32✔
1807
                while (true) {
32✔
1808
                        $i += 1;
32✔
1809
                        if (! isset($tokens[$i])) {
32✔
UNCOV
1810
                                return false;
×
1811
                        }
1812
                        if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $i)) {
32✔
1813
                                return ($tokens[$i]['code'] === T_VARIABLE);
32✔
1814
                        }
1815
                }
8✔
1816
        }
1817

1818
        /**
1819
         * Return true if the token is inside an abstract class.
1820
         *
1821
         * @param File $phpcsFile
1822
         * @param int  $stackPtr
1823
         *
1824
         * @return bool
1825
         */
1826
        public static function isInAbstractClass(File $phpcsFile, $stackPtr)
108✔
1827
        {
1828
                $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
108✔
1829
                if (! is_int($classIndex)) {
108✔
1830
                        return false;
92✔
1831
                }
1832
                $classProperties = $phpcsFile->getClassProperties($classIndex);
16✔
1833
                return $classProperties['is_abstract'];
16✔
1834
        }
1835

1836
        /**
1837
         * Return true if the function body is empty or contains only `return;`
1838
         *
1839
         * @param File $phpcsFile
1840
         * @param int  $stackPtr  The index of the function keyword.
1841
         *
1842
         * @return bool
1843
         */
1844
        public static function isFunctionBodyEmpty(File $phpcsFile, $stackPtr)
8✔
1845
        {
1846
                $tokens = $phpcsFile->getTokens();
8✔
1847
                if ($tokens[$stackPtr]['code'] !== T_FUNCTION) {
8✔
UNCOV
1848
                        return false;
×
1849
                }
1850
                $functionScopeStart = $tokens[$stackPtr]['scope_opener'];
8✔
1851
                $functionScopeEnd = $tokens[$stackPtr]['scope_closer'];
8✔
1852
                $tokensToIgnore = array_merge(
8✔
1853
                        Tokens::$emptyTokens,
8✔
1854
                        [
4✔
1855
                                T_RETURN,
8✔
1856
                                T_SEMICOLON,
8✔
1857
                                T_OPEN_CURLY_BRACKET,
8✔
1858
                                T_CLOSE_CURLY_BRACKET,
8✔
1859
                        ]
4✔
1860
                );
6✔
1861
                for ($i = $functionScopeStart; $i < $functionScopeEnd; $i++) {
8✔
1862
                        if (! in_array($tokens[$i]['code'], $tokensToIgnore, true)) {
8✔
1863
                                return false;
8✔
1864
                        }
1865
                }
2✔
1866
                return true;
8✔
1867
        }
1868
}
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