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

sirbrillig / phpcs-variable-analysis / 8412321847

24 Mar 2024 09:44PM UTC coverage: 93.851% (-0.1%) from 93.95%
8412321847

push

github

web-flow
Make sure that recursive search of list assignments stays inside them (#318)

* Add tests for destructuring assignment with array arg

* Make sure that recursive search of list assignments stays inside them

19 of 21 new or added lines in 1 file covered. (90.48%)

1 existing line in 1 file now uncovered.

1862 of 1984 relevant lines covered (93.85%)

133.97 hits per line

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

89.44
/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\ForLoopInfo;
8
use VariableAnalysis\Lib\EnumInfo;
9
use VariableAnalysis\Lib\ScopeType;
10
use VariableAnalysis\Lib\VariableInfo;
11
use PHP_CodeSniffer\Util\Tokens;
12

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

381
                return $argPtrs;
32✔
382
        }
383

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

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

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

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

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

445
                return self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
344✔
446
        }
447

448
        /**
449
         * Return the token index of the scope start for a token
450
         *
451
         * For a variable within a function body, or a variable within a function
452
         * definition argument list, this will return the function keyword's index.
453
         *
454
         * For a variable within a "use" import list within a function definition,
455
         * this will return the enclosing scope, not the function keyword. This is
456
         * important to note because the "use" keyword performs double-duty, defining
457
         * variables for the function's scope, and consuming the variables in the
458
         * enclosing scope. Use `getUseIndexForUseImport` to determine if this
459
         * token needs to be treated as a "use".
460
         *
461
         * For a variable within an arrow function definition argument list,
462
         * this will return the arrow function's keyword index.
463
         *
464
         * For a variable in an arrow function body, this will return the enclosing
465
         * function's index, which may be incorrect.
466
         *
467
         * Since a variable in an arrow function's body may be imported from the
468
         * enclosing scope, it's important to test to see if the variable is in an
469
         * arrow function and also check its enclosing scope separately.
470
         *
471
         * @param File $phpcsFile
472
         * @param int  $stackPtr
473
         *
474
         * @return ?int
475
         */
476
        public static function findVariableScopeExceptArrowFunctions(File $phpcsFile, $stackPtr)
344✔
477
        {
478
                $tokens = $phpcsFile->getTokens();
344✔
479
                $allowedTypes = [
172✔
480
                        T_VARIABLE,
344✔
481
                        T_DOUBLE_QUOTED_STRING,
344✔
482
                        T_HEREDOC,
344✔
483
                        T_STRING,
344✔
484
                ];
344✔
485
                if (! in_array($tokens[$stackPtr]['code'], $allowedTypes, true)) {
344✔
486
                        throw new \Exception("Cannot find variable scope for non-variable {$tokens[$stackPtr]['type']}");
×
487
                }
488

489
                $startOfTokenScope = self::getStartOfTokenScope($phpcsFile, $stackPtr);
344✔
490
                if (is_int($startOfTokenScope) && $startOfTokenScope > 0) {
344✔
491
                        return $startOfTokenScope;
332✔
492
                }
493

494
                // If there is no "conditions" array, this is a function definition argument.
495
                if (self::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
280✔
496
                        $functionPtr = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
228✔
497
                        if (! is_int($functionPtr)) {
228✔
498
                                throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
499
                        }
500
                        return $functionPtr;
228✔
501
                }
502

503
                self::debug('Cannot find function scope for variable at', $stackPtr);
108✔
504
                return $startOfTokenScope;
108✔
505
        }
506

507
        /**
508
         * Return the token index of the scope start for a variable token
509
         *
510
         * This will only work for a variable within a function's body. Otherwise,
511
         * see `findVariableScope`, which is more complex.
512
         *
513
         * Note that if used on a variable in an arrow function, it will return the
514
         * enclosing function's scope, which may be incorrect.
515
         *
516
         * @param File $phpcsFile
517
         * @param int  $stackPtr
518
         *
519
         * @return ?int
520
         */
521
        private static function getStartOfTokenScope(File $phpcsFile, $stackPtr)
344✔
522
        {
523
                $tokens = $phpcsFile->getTokens();
344✔
524
                $token = $tokens[$stackPtr];
344✔
525

526
                $inClass = false;
344✔
527
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
344✔
528
                $functionTokenTypes = [
172✔
529
                        T_FUNCTION,
344✔
530
                        T_CLOSURE,
344✔
531
                ];
344✔
532
                foreach (array_reverse($conditions, true) as $scopePtr => $scopeCode) {
344✔
533
                        if (in_array($scopeCode, $functionTokenTypes, true) || self::isArrowFunction($phpcsFile, $scopePtr)) {
336✔
534
                                return $scopePtr;
332✔
535
                        }
536
                        if (isset(Tokens::$ooScopeTokens[$scopeCode]) === true) {
160✔
537
                                $inClass = true;
68✔
538
                        }
34✔
539
                }
158✔
540

541
                if ($inClass) {
280✔
542
                        // If this is inside a class and not inside a function, this is either a
543
                        // class member variable definition, or a function argument. If it is a
544
                        // variable definition, it has no scope on its own (it can only be used
545
                        // with an object reference). If it is a function argument, we need to do
546
                        // more work (see `findVariableScopeExceptArrowFunctions`).
547
                        return null;
60✔
548
                }
549

550
                // If we can't find a scope, let's use the first token of the file.
551
                return 0;
240✔
552
        }
553

554
        /**
555
         * @param File $phpcsFile
556
         * @param int  $stackPtr
557
         *
558
         * @return bool
559
         */
560
        public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $stackPtr)
×
561
        {
562
                $tokens = $phpcsFile->getTokens();
×
563
                $token = $tokens[$stackPtr];
×
564
                $openParenIndices = isset($token['nested_parenthesis']) ? $token['nested_parenthesis'] : [];
×
565
                if (empty($openParenIndices)) {
×
566
                        return false;
×
567
                }
568
                $openParenPtr = $openParenIndices[0];
×
569
                return self::isArrowFunction($phpcsFile, $openParenPtr - 1);
×
570
        }
571

572
        /**
573
         * @param File $phpcsFile
574
         * @param int  $stackPtr
575
         *
576
         * @return ?int
577
         */
578
        public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr)
344✔
579
        {
580
                $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr);
344✔
581
                if (! is_int($arrowFunctionIndex)) {
344✔
582
                        return null;
344✔
583
                }
584
                $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex);
24✔
585
                if (! $arrowFunctionInfo) {
24✔
586
                        return null;
×
587
                }
588
                $arrowFunctionScopeStart = $arrowFunctionInfo['scope_opener'];
24✔
589
                $arrowFunctionScopeEnd = $arrowFunctionInfo['scope_closer'];
24✔
590
                if ($stackPtr > $arrowFunctionScopeStart && $stackPtr < $arrowFunctionScopeEnd) {
24✔
591
                        return $arrowFunctionIndex;
24✔
592
                }
593
                return null;
24✔
594
        }
595

596
        /**
597
         * @param File $phpcsFile
598
         * @param int  $stackPtr
599
         *
600
         * @return ?int
601
         */
602
        private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr)
344✔
603
        {
604
                $tokens = $phpcsFile->getTokens();
344✔
605
                $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
344✔
606
                for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) {
344✔
607
                        $token = $tokens[$index];
344✔
608
                        if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) {
344✔
609
                                return $index;
24✔
610
                        }
611
                }
172✔
612
                return null;
344✔
613
        }
614

615
        /**
616
         * @param File $phpcsFile
617
         * @param int  $stackPtr
618
         *
619
         * @return bool
620
         */
621
        public static function isArrowFunction(File $phpcsFile, $stackPtr)
344✔
622
        {
623
                $tokens = $phpcsFile->getTokens();
344✔
624
                if (defined('T_FN') && $tokens[$stackPtr]['code'] === T_FN) {
344✔
625
                        return true;
24✔
626
                }
627
                if ($tokens[$stackPtr]['content'] !== 'fn') {
344✔
628
                        return false;
344✔
629
                }
630
                // Make sure next non-space token is an open parenthesis
631
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
×
632
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
×
633
                        return false;
×
634
                }
635
                // Find the associated close parenthesis
636
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
×
637
                // Make sure the next token is a fat arrow
638
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
×
639
                if (! is_int($fatArrowIndex)) {
×
640
                        return false;
×
641
                }
642
                if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') {
×
643
                        return false;
×
644
                }
645
                return true;
×
646
        }
647

648
        /**
649
         * @param File $phpcsFile
650
         * @param int  $stackPtr
651
         *
652
         * @return ?array<string, int>
653
         */
654
        public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
24✔
655
        {
656
                $tokens = $phpcsFile->getTokens();
24✔
657
                if ($tokens[$stackPtr]['content'] !== 'fn') {
24✔
658
                        return null;
×
659
                }
660
                // Make sure next non-space token is an open parenthesis
661
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
24✔
662
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
24✔
663
                        return null;
×
664
                }
665
                // Find the associated close parenthesis
666
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
24✔
667
                // Make sure the next token is a fat arrow or a return type
668
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
24✔
669
                if (! is_int($fatArrowIndex)) {
24✔
670
                        return null;
×
671
                }
672
                if (
673
                        $tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW &&
24✔
674
                        $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW' &&
24✔
675
                        $tokens[$fatArrowIndex]['code'] !== T_COLON
16✔
676
                ) {
12✔
677
                        return null;
×
678
                }
679

680
                // Find the scope closer
681
                $scopeCloserIndex = null;
24✔
682
                $foundCurlyPairs = 0;
24✔
683
                $foundArrayPairs = 0;
24✔
684
                $foundParenPairs = 0;
24✔
685
                $arrowBodyStart = $tokens[$stackPtr]['parenthesis_closer'] + 1;
24✔
686
                $lastToken = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
24✔
687
                for ($index = $arrowBodyStart; $index < $lastToken; $index++) {
24✔
688
                        $token = $tokens[$index];
24✔
689
                        if (empty($token['code'])) {
24✔
690
                                $scopeCloserIndex = $index;
×
691
                                break;
×
692
                        }
693

694
                        $code = $token['code'];
24✔
695

696
                        // A semicolon is always a closer.
697
                        if ($code === T_SEMICOLON) {
24✔
698
                                $scopeCloserIndex = $index;
16✔
699
                                break;
16✔
700
                        }
701

702
                        // Track pair opening tokens.
703
                        if ($code === T_OPEN_CURLY_BRACKET) {
24✔
704
                                $foundCurlyPairs += 1;
8✔
705
                                continue;
8✔
706
                        }
707
                        if ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) {
24✔
708
                                $foundArrayPairs += 1;
16✔
709
                                continue;
16✔
710
                        }
711
                        if ($code === T_OPEN_PARENTHESIS) {
24✔
712
                                $foundParenPairs += 1;
16✔
713
                                continue;
16✔
714
                        }
715

716
                        // A pair closing is only an arrow func closer if there was no matching opening token.
717
                        if ($code === T_CLOSE_CURLY_BRACKET) {
24✔
718
                                if ($foundCurlyPairs === 0) {
×
719
                                        $scopeCloserIndex = $index;
×
720
                                        break;
×
721
                                }
722
                                $foundCurlyPairs -= 1;
×
723
                                continue;
×
724
                        }
725
                        if ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) {
24✔
726
                                if ($foundArrayPairs === 0) {
16✔
727
                                        $scopeCloserIndex = $index;
×
728
                                        break;
×
729
                                }
730
                                $foundArrayPairs -= 1;
16✔
731
                                continue;
16✔
732
                        }
733
                        if ($code === T_CLOSE_PARENTHESIS) {
24✔
734
                                if ($foundParenPairs === 0) {
16✔
735
                                        $scopeCloserIndex = $index;
8✔
736
                                        break;
8✔
737
                                }
738
                                $foundParenPairs -= 1;
16✔
739
                                continue;
16✔
740
                        }
741

742
                        // A comma is a closer only if we are not inside an opening token.
743
                        if ($code === T_COMMA) {
24✔
744
                                if (empty($foundArrayPairs) && empty($foundParenPairs) && empty($foundCurlyPairs)) {
16✔
745
                                        $scopeCloserIndex = $index;
16✔
746
                                        break;
16✔
747
                                }
748
                                continue;
8✔
749
                        }
750
                }
12✔
751

752
                if (! is_int($scopeCloserIndex)) {
24✔
753
                        return null;
×
754
                }
755

756
                return [
12✔
757
                        'scope_opener' => $stackPtr,
24✔
758
                        'scope_closer' => $scopeCloserIndex,
24✔
759
                ];
24✔
760
        }
761

762
        /**
763
         * Determine if a token is a list opener for list assignment/destructuring.
764
         *
765
         * The index provided can be either the opening square brace of a short list
766
         * assignment like the first character of `[$a] = $b;` or the `list` token of
767
         * an expression like `list($a) = $b;` or the opening parenthesis of that
768
         * expression.
769
         *
770
         * @param File $phpcsFile
771
         * @param int  $listOpenerIndex
772
         *
773
         * @return bool
774
         */
775
        private static function isListAssignment(File $phpcsFile, $listOpenerIndex)
32✔
776
        {
777
                $tokens = $phpcsFile->getTokens();
32✔
778
                // Match `[$a] = $b;` except for when the previous token is a parenthesis.
779
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SHORT_ARRAY) {
32✔
780
                        return true;
28✔
781
                }
782
                // Match `list($a) = $b;`
783
                if ($tokens[$listOpenerIndex]['code'] === T_LIST) {
32✔
784
                        return true;
32✔
785
                }
786

787
                // If $listOpenerIndex is the open parenthesis of `list($a) = $b;`, then
788
                // match that too.
789
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_PARENTHESIS) {
16✔
790
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
4✔
791
                        if (
792
                                isset($tokens[$previousTokenPtr])
4✔
793
                                && $tokens[$previousTokenPtr]['code'] === T_LIST
4✔
794
                        ) {
2✔
795
                                return true;
4✔
796
                        }
797
                        return true;
×
798
                }
799

800
                // If the list opener token is a square bracket that is preceeded by a
801
                // close parenthesis that has an owner which is a scope opener, then this
802
                // is a list assignment and not an array access.
803
                //
804
                // Match `if (true) [$a] = $b;`
805
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SQUARE_BRACKET) {
12✔
806
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
12✔
807
                        if (
808
                                isset($tokens[$previousTokenPtr])
12✔
809
                                && $tokens[$previousTokenPtr]['code'] === T_CLOSE_PARENTHESIS
12✔
810
                                && isset($tokens[$previousTokenPtr]['parenthesis_owner'])
12✔
811
                                && isset(Tokens::$scopeOpeners[$tokens[$tokens[$previousTokenPtr]['parenthesis_owner']]['code']])
12✔
812
                        ) {
6✔
813
                                return true;
4✔
814
                        }
815
                }
4✔
816

817
                return false;
8✔
818
        }
819

820
        /**
821
         * Return a list of indices for variables assigned within a list assignment.
822
         *
823
         * The index provided can be either the opening square brace of a short list
824
         * assignment like the first character of `[$a] = $b;` or the `list` token of
825
         * an expression like `list($a) = $b;` or the opening parenthesis of that
826
         * expression.
827
         *
828
         * @param File $phpcsFile
829
         * @param int  $listOpenerIndex
830
         *
831
         * @return ?array<int>
832
         */
833
        public static function getListAssignments(File $phpcsFile, $listOpenerIndex)
48✔
834
        {
835
                $tokens = $phpcsFile->getTokens();
48✔
836
                self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]);
48✔
837

838
                // First find the end of the list
839
                $closePtr = null;
48✔
840
                if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) {
48✔
841
                        $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer'];
48✔
842
                }
24✔
843
                if (isset($tokens[$listOpenerIndex]['bracket_closer'])) {
48✔
844
                        $closePtr = $tokens[$listOpenerIndex]['bracket_closer'];
48✔
845
                }
24✔
846
                if (! $closePtr) {
48✔
847
                        return null;
×
848
                }
849

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

853
                // If the next token isn't an assignment, check for nested brackets because we might be a nested assignment
854
                if (! is_int($assignPtr) || $tokens[$assignPtr]['code'] !== T_EQUAL) {
48✔
855
                        // Collect the enclosing list open/close tokens ($parents is an assoc array keyed by opener index and the value is the closer index)
856
                        $parents = isset($tokens[$listOpenerIndex]['nested_parenthesis']) ? $tokens[$listOpenerIndex]['nested_parenthesis'] : [];
40✔
857
                        // There's no record of nested brackets for short lists; we'll have to find the parent ourselves
858
                        if (empty($parents)) {
40✔
859
                                $parentSquareBracketPtr = self::findContainingOpeningSquareBracket($phpcsFile, $listOpenerIndex);
40✔
860
                                if (is_int($parentSquareBracketPtr)) {
40✔
861
                                        // Make sure that the parent is really a parent by checking that its
862
                                        // closing index is outside of the current bracket's closing index.
863
                                        $parentSquareBracketToken = $tokens[$parentSquareBracketPtr];
4✔
864
                                        $parentSquareBracketClosePtr = $parentSquareBracketToken['bracket_closer'];
4✔
865
                                        if ($parentSquareBracketClosePtr && $parentSquareBracketClosePtr > $closePtr) {
4✔
866
                                                self::debug("found enclosing bracket for {$listOpenerIndex}: {$parentSquareBracketPtr}");
4✔
867
                                                // Collect the opening index, but we don't actually need the closing paren index so just make that 0
868
                                                $parents = [$parentSquareBracketPtr => 0];
4✔
869
                                        }
2✔
870
                                }
2✔
871
                        }
20✔
872
                        // If we have no parents, this is not a nested assignment and therefore is not an assignment
873
                        if (empty($parents)) {
40✔
874
                                return null;
40✔
875
                        }
876

877
                        // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion)
878
                        $isNestedAssignment = null;
28✔
879
                        $parentListOpener = array_keys(array_reverse($parents, true))[0];
28✔
880
                        $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener);
28✔
881
                        if ($isNestedAssignment === null) {
28✔
882
                                return null;
28✔
883
                        }
884
                }
2✔
885

886
                $variablePtrs = [];
32✔
887

888
                $currentPtr = $listOpenerIndex;
32✔
889
                $variablePtr = 0;
32✔
890
                while ($currentPtr < $closePtr && is_int($variablePtr)) {
32✔
891
                        $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr);
32✔
892
                        if (is_int($variablePtr)) {
32✔
893
                                $variablePtrs[] = $variablePtr;
32✔
894
                        }
16✔
895
                        ++$currentPtr;
32✔
896
                }
16✔
897

898
                if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) {
32✔
899
                        return null;
8✔
900
                }
901

902
                return $variablePtrs;
32✔
903
        }
904

905
        /**
906
         * @param File $phpcsFile
907
         * @param int  $stackPtr
908
         *
909
         * @return string[]
910
         */
911
        public static function getVariablesDefinedByArrowFunction(File $phpcsFile, $stackPtr)
24✔
912
        {
913
                $tokens = $phpcsFile->getTokens();
24✔
914
                $arrowFunctionToken = $tokens[$stackPtr];
24✔
915
                $variableNames = [];
24✔
916
                self::debug('looking for variables in arrow function token', $arrowFunctionToken);
24✔
917
                for ($index = $arrowFunctionToken['parenthesis_opener']; $index < $arrowFunctionToken['parenthesis_closer']; $index++) {
24✔
918
                        $token = $tokens[$index];
24✔
919
                        if ($token['code'] === T_VARIABLE) {
24✔
920
                                $variableNames[] = self::normalizeVarName($token['content']);
24✔
921
                        }
12✔
922
                }
12✔
923
                self::debug('found these variables in arrow function token', $variableNames);
24✔
924
                return $variableNames;
24✔
925
        }
926

927
        /**
928
         * @return void
929
         */
930
        public static function debug()
344✔
931
        {
932
                $messages = func_get_args();
344✔
933
                if (! defined('PHP_CODESNIFFER_VERBOSITY')) {
344✔
934
                        return;
×
935
                }
936
                if (PHP_CODESNIFFER_VERBOSITY <= 3) {
344✔
937
                        return;
344✔
938
                }
939
                $output = PHP_EOL . 'VariableAnalysisSniff: DEBUG:';
×
940
                foreach ($messages as $message) {
×
941
                        if (is_string($message) || is_numeric($message)) {
×
942
                                $output .= ' "' . $message . '"';
×
943
                                continue;
×
944
                        }
945
                        $output .= PHP_EOL . var_export($message, true) . PHP_EOL;
×
946
                }
947
                $output .= PHP_EOL;
×
948
                echo $output;
×
949
        }
950

951
        /**
952
         * @param string $pattern
953
         * @param string $value
954
         *
955
         * @return string[]
956
         */
957
        public static function splitStringToArray($pattern, $value)
24✔
958
        {
959
                if (empty($pattern)) {
24✔
960
                        return [];
×
961
                }
962
                $result = preg_split($pattern, $value);
24✔
963
                return is_array($result) ? $result : [];
24✔
964
        }
965

966
        /**
967
         * @param string $varName
968
         *
969
         * @return bool
970
         */
971
        public static function isVariableANumericVariable($varName)
328✔
972
        {
973
                return is_numeric(substr($varName, 0, 1));
328✔
974
        }
975

976
        /**
977
         * @param File $phpcsFile
978
         * @param int  $stackPtr
979
         *
980
         * @return bool
981
         */
982
        public static function isVariableInsideElseCondition(File $phpcsFile, $stackPtr)
320✔
983
        {
984
                $tokens = $phpcsFile->getTokens();
320✔
985
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
320✔
986
                $nonFunctionTokenTypes[] = T_OPEN_PARENTHESIS;
320✔
987
                $nonFunctionTokenTypes[] = T_INLINE_HTML;
320✔
988
                $nonFunctionTokenTypes[] = T_CLOSE_TAG;
320✔
989
                $nonFunctionTokenTypes[] = T_VARIABLE;
320✔
990
                $nonFunctionTokenTypes[] = T_ELLIPSIS;
320✔
991
                $nonFunctionTokenTypes[] = T_COMMA;
320✔
992
                $nonFunctionTokenTypes[] = T_STRING;
320✔
993
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
320✔
994
                $elsePtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $stackPtr - 1, null, true, null, true));
320✔
995
                $elseTokenTypes = [
160✔
996
                        T_ELSE,
320✔
997
                        T_ELSEIF,
320✔
998
                ];
320✔
999
                if (is_int($elsePtr) && in_array($tokens[$elsePtr]['code'], $elseTokenTypes, true)) {
320✔
1000
                        return true;
16✔
1001
                }
1002
                return false;
320✔
1003
        }
1004

1005
        /**
1006
         * @param File $phpcsFile
1007
         * @param int  $stackPtr
1008
         *
1009
         * @return bool
1010
         */
1011
        public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr)
320✔
1012
        {
1013
                $tokens = $phpcsFile->getTokens();
320✔
1014
                $token = $tokens[$stackPtr];
320✔
1015
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
320✔
1016
                $elseTokenTypes = [
160✔
1017
                        T_ELSE,
320✔
1018
                        T_ELSEIF,
320✔
1019
                ];
320✔
1020
                foreach (array_reverse($conditions, true) as $scopeCode) {
320✔
1021
                        if (in_array($scopeCode, $elseTokenTypes, true)) {
308✔
1022
                                return true;
16✔
1023
                        }
1024
                }
160✔
1025

1026
                // Some else body code will not have conditions because it is inline (no
1027
                // curly braces) so we have to look in other ways.
1028
                $previousSemicolonPtr = $phpcsFile->findPrevious([T_SEMICOLON], $stackPtr - 1);
320✔
1029
                if (! is_int($previousSemicolonPtr)) {
320✔
1030
                        $previousSemicolonPtr = 0;
148✔
1031
                }
74✔
1032
                $elsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1, $previousSemicolonPtr);
320✔
1033
                if (is_int($elsePtr)) {
320✔
1034
                        return true;
8✔
1035
                }
1036

1037
                return false;
320✔
1038
        }
1039

1040
        /**
1041
         * @param File $phpcsFile
1042
         * @param int  $stackPtr
1043
         *
1044
         * @return int[]
1045
         */
1046
        public static function getAttachedBlockIndicesForElse(File $phpcsFile, $stackPtr)
24✔
1047
        {
1048
                $currentElsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1);
24✔
1049
                if (! is_int($currentElsePtr)) {
24✔
1050
                        throw new \Exception("Cannot find expected else at {$stackPtr}");
×
1051
                }
1052

1053
                $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
24✔
1054
                if (! is_int($ifPtr)) {
24✔
1055
                        throw new \Exception("Cannot find if for else at {$stackPtr}");
×
1056
                }
1057
                $blockIndices = [$ifPtr];
24✔
1058

1059
                $previousElseIfPtr = $currentElsePtr;
24✔
1060
                do {
1061
                        $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
24✔
1062
                        if (is_int($elseIfPtr)) {
24✔
1063
                                $blockIndices[] = $elseIfPtr;
16✔
1064
                                $previousElseIfPtr = $elseIfPtr;
16✔
1065
                        }
8✔
1066
                } while (is_int($elseIfPtr));
24✔
1067

1068
                return $blockIndices;
24✔
1069
        }
1070

1071
        /**
1072
         * @param int $needle
1073
         * @param int $scopeStart
1074
         * @param int $scopeEnd
1075
         *
1076
         * @return bool
1077
         */
1078
        public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
24✔
1079
        {
1080
                return ($needle > $scopeStart && $needle < $scopeEnd);
24✔
1081
        }
1082

1083
        /**
1084
         * @param File $phpcsFile
1085
         * @param int  $scopeStartIndex
1086
         *
1087
         * @return int
1088
         */
1089
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
344✔
1090
        {
1091
                $tokens = $phpcsFile->getTokens();
344✔
1092
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
344✔
1093

1094
                if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) {
344✔
1095
                        $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex);
24✔
1096
                        $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex;
24✔
1097
                }
12✔
1098

1099
                if ($scopeStartIndex === 0) {
344✔
1100
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
344✔
1101
                }
172✔
1102
                return $scopeCloserIndex;
344✔
1103
        }
1104

1105
        /**
1106
         * @param File $phpcsFile
1107
         *
1108
         * @return int
1109
         */
1110
        public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
344✔
1111
        {
1112
                $tokens = $phpcsFile->getTokens();
344✔
1113
                foreach (array_reverse($tokens, true) as $index => $token) {
344✔
1114
                        if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
344✔
1115
                                return $index;
344✔
1116
                        }
1117
                }
168✔
1118
                self::debug('no non-empty token found for end of file');
×
1119
                return 0;
×
1120
        }
1121

1122
        /**
1123
         * @param VariableInfo $varInfo
1124
         * @param ScopeInfo    $scopeInfo
1125
         *
1126
         * @return bool
1127
         */
1128
        public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo)
64✔
1129
        {
1130
                $foundVarPosition = false;
64✔
1131
                foreach ($scopeInfo->variables as $variable) {
64✔
1132
                        if ($variable === $varInfo) {
64✔
1133
                                $foundVarPosition = true;
64✔
1134
                                continue;
64✔
1135
                        }
1136
                        if (! $foundVarPosition) {
44✔
1137
                                continue;
36✔
1138
                        }
1139
                        if ($variable->scopeType !== ScopeType::PARAM) {
44✔
1140
                                continue;
36✔
1141
                        }
1142
                        if ($variable->firstRead) {
16✔
1143
                                return true;
16✔
1144
                        }
1145
                }
32✔
1146
                return false;
64✔
1147
        }
1148

1149
        /**
1150
         * @param File         $phpcsFile
1151
         * @param VariableInfo $varInfo
1152
         * @param ScopeInfo    $scopeInfo
1153
         *
1154
         * @return bool
1155
         */
1156
        public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
4✔
1157
        {
1158
                $requireTokens = [
2✔
1159
                        T_REQUIRE,
4✔
1160
                        T_REQUIRE_ONCE,
4✔
1161
                        T_INCLUDE,
4✔
1162
                        T_INCLUDE_ONCE,
4✔
1163
                ];
4✔
1164
                $indexToStartSearch = $varInfo->firstDeclared;
4✔
1165
                if (! empty($varInfo->firstInitialized)) {
4✔
1166
                        $indexToStartSearch = $varInfo->firstInitialized;
4✔
1167
                }
2✔
1168
                $tokens = $phpcsFile->getTokens();
4✔
1169
                $indexToStopSearch = isset($tokens[$scopeInfo->scopeStartIndex]['scope_closer']) ? $tokens[$scopeInfo->scopeStartIndex]['scope_closer'] : null;
4✔
1170
                if (! is_int($indexToStartSearch) || ! is_int($indexToStopSearch)) {
4✔
1171
                        return false;
×
1172
                }
1173
                $requireTokenIndex = $phpcsFile->findNext($requireTokens, $indexToStartSearch + 1, $indexToStopSearch);
4✔
1174
                if (is_int($requireTokenIndex)) {
4✔
1175
                        return true;
4✔
1176
                }
1177
                return false;
×
1178
        }
1179

1180
        /**
1181
         * Find the index of the function keyword for a token in a function call's arguments
1182
         *
1183
         * For the variable `$foo` in the expression `doSomething($foo)`, this will
1184
         * return the index of the `doSomething` token.
1185
         *
1186
         * @param File $phpcsFile
1187
         * @param int  $stackPtr
1188
         *
1189
         * @return ?int
1190
         */
1191
        public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr)
332✔
1192
        {
1193
                $tokens = $phpcsFile->getTokens();
332✔
1194
                $token = $tokens[$stackPtr];
332✔
1195
                if (empty($token['nested_parenthesis'])) {
332✔
1196
                        return null;
324✔
1197
                }
1198
                /**
1199
                 * @var array<int|string|null>
1200
                 */
1201
                $startingParenthesis = array_keys($token['nested_parenthesis']);
212✔
1202
                $startOfArguments = end($startingParenthesis);
212✔
1203
                if (! is_int($startOfArguments)) {
212✔
1204
                        return null;
×
1205
                }
1206

1207
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
212✔
1208
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
212✔
1209
                if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
212✔
1210
                        return null;
×
1211
                }
1212
                if (
1213
                        $tokens[$functionPtr]['content'] === 'function'
212✔
1214
                        || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))
212✔
1215
                ) {
106✔
1216
                        // If there is a function/fn keyword before the beginning of the parens,
1217
                        // this is a function definition and not a function call.
1218
                        return null;
×
1219
                }
1220
                if (! empty($tokens[$functionPtr]['scope_opener'])) {
212✔
1221
                        // If the alleged function name has a scope, this is not a function call.
1222
                        return null;
138✔
1223
                }
1224

1225
                $functionNameType = $tokens[$functionPtr]['code'];
178✔
1226
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
178✔
1227
                        // If the alleged function name is not a variable or a string, this is
1228
                        // not a function call.
1229
                        return null;
48✔
1230
                }
1231

1232
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
174✔
1233
                        // If the variable is inside a different scope than the function name,
1234
                        // the function call doesn't apply to the variable.
1235
                        return null;
28✔
1236
                }
1237

1238
                return $functionPtr;
174✔
1239
        }
1240

1241
        /**
1242
         * @param File $phpcsFile
1243
         * @param int  $stackPtr
1244
         *
1245
         * @return bool
1246
         */
1247
        public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
320✔
1248
        {
1249
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
320✔
1250
                if (! is_int($functionIndex)) {
320✔
1251
                        return false;
302✔
1252
                }
1253
                $tokens = $phpcsFile->getTokens();
174✔
1254
                if (! isset($tokens[$functionIndex])) {
174✔
1255
                        return false;
×
1256
                }
1257
                $allowedFunctionNames = [
87✔
1258
                        'isset',
174✔
1259
                        'empty',
174✔
1260
                ];
174✔
1261
                if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) {
174✔
1262
                        return true;
20✔
1263
                }
1264
                return false;
166✔
1265
        }
1266

1267
        /**
1268
         * @param File $phpcsFile
1269
         * @param int  $stackPtr
1270
         *
1271
         * @return bool
1272
         */
1273
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
248✔
1274
        {
1275
                $tokens = $phpcsFile->getTokens();
248✔
1276
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
248✔
1277

1278
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
248✔
1279
                if (! is_int($arrayPushOperatorIndex1)) {
248✔
1280
                        return false;
×
1281
                }
1282
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
248✔
1283
                        return false;
248✔
1284
                }
1285

1286
                $arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true));
36✔
1287
                if (! is_int($arrayPushOperatorIndex2)) {
36✔
1288
                        return false;
×
1289
                }
1290
                if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') {
36✔
1291
                        return false;
8✔
1292
                }
1293

1294
                $arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true));
28✔
1295
                if (! is_int($arrayPushOperatorIndex3)) {
28✔
1296
                        return false;
×
1297
                }
1298
                if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') {
28✔
1299
                        return false;
×
1300
                }
1301

1302
                return true;
28✔
1303
        }
1304

1305
        /**
1306
         * @param File $phpcsFile
1307
         * @param int  $stackPtr
1308
         *
1309
         * @return bool
1310
         */
1311
        public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
248✔
1312
        {
1313
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
248✔
1314
                if (! is_int($functionIndex)) {
248✔
1315
                        return false;
218✔
1316
                }
1317
                $tokens = $phpcsFile->getTokens();
98✔
1318
                if (! isset($tokens[$functionIndex])) {
98✔
1319
                        return false;
×
1320
                }
1321
                if ($tokens[$functionIndex]['content'] === 'unset') {
98✔
1322
                        return true;
8✔
1323
                }
1324
                return false;
90✔
1325
        }
1326

1327
        /**
1328
         * @param File $phpcsFile
1329
         * @param int  $stackPtr
1330
         *
1331
         * @return bool
1332
         */
1333
        public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr)
312✔
1334
        {
1335
                $previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
312✔
1336
                if (! is_int($previousStatementPtr)) {
312✔
1337
                        $previousStatementPtr = 1;
32✔
1338
                }
16✔
1339
                $previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
312✔
1340
                if (is_int($previousTokenPtr)) {
312✔
1341
                        return true;
4✔
1342
                }
1343
                return false;
312✔
1344
        }
1345

1346
        /**
1347
         * @param File $phpcsFile
1348
         * @param int  $stackPtr
1349
         *
1350
         * @return bool
1351
         */
1352
        public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
332✔
1353
        {
1354
                // Is the next non-whitespace an assignment?
1355
                $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
332✔
1356
                if (! is_int($assignPtr)) {
332✔
1357
                        return false;
324✔
1358
                }
1359

1360
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1361
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
312✔
1362
                        self::debug('found variable variable');
4✔
1363
                        return false;
4✔
1364
                }
1365
                return true;
312✔
1366
        }
1367

1368
        /**
1369
         * @param File $phpcsFile
1370
         * @param int  $stackPtr
1371
         *
1372
         * @return bool
1373
         */
1374
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
312✔
1375
        {
1376
                $tokens = $phpcsFile->getTokens();
312✔
1377

1378
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
312✔
1379
                if ($prev === false) {
312✔
1380
                        return false;
×
1381
                }
1382
                if ($tokens[$prev]['code'] === T_DOLLAR) {
312✔
1383
                        return true;
4✔
1384
                }
1385
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
312✔
1386
                        return false;
256✔
1387
                }
1388

1389
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
228✔
1390
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
228✔
1391
                        return true;
×
1392
                }
1393
                return false;
228✔
1394
        }
1395

1396
        /**
1397
         * @param File $phpcsFile
1398
         * @param int  $stackPtr
1399
         *
1400
         * @return EnumInfo|null
1401
         */
1402
        public static function makeEnumInfo(File $phpcsFile, $stackPtr)
4✔
1403
        {
1404
                $tokens = $phpcsFile->getTokens();
4✔
1405
                $token = $tokens[$stackPtr];
4✔
1406

1407
                if (isset($token['scope_opener'])) {
4✔
1408
                        $blockStart = $token['scope_opener'];
2✔
1409
                        $blockEnd = $token['scope_closer'];
2✔
1410
                } else {
1✔
1411
                        // Enums before phpcs could detect them do not have scopes so we have to
1412
                        // find them ourselves.
1413

1414
                        $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
4✔
1415
                        if (! is_int($blockStart)) {
4✔
1416
                                return null;
4✔
1417
                        }
1418
                        $blockEnd = $tokens[$blockStart]['bracket_closer'];
2✔
1419
                }
1420

1421
                return new EnumInfo(
4✔
1422
                        $stackPtr,
4✔
1423
                        $blockStart,
4✔
1424
                        $blockEnd
2✔
1425
                );
4✔
1426
        }
1427

1428
        /**
1429
         * @param File $phpcsFile
1430
         * @param int  $stackPtr
1431
         *
1432
         * @return ForLoopInfo
1433
         */
1434
        public static function makeForLoopInfo(File $phpcsFile, $stackPtr)
8✔
1435
        {
1436
                $tokens = $phpcsFile->getTokens();
8✔
1437
                $token = $tokens[$stackPtr];
8✔
1438
                $forIndex = $stackPtr;
8✔
1439
                $blockStart = $token['parenthesis_closer'];
8✔
1440
                if (isset($token['scope_opener'])) {
8✔
1441
                        $blockStart = $token['scope_opener'];
8✔
1442
                        $blockEnd = $token['scope_closer'];
8✔
1443
                } else {
4✔
1444
                        // Some for loop blocks will not have scope positions because it they are
1445
                        // inline (no curly braces) so we have to find the end of their scope by
1446
                        // looking for the end of the next statement.
1447
                        $nextSemicolonIndex = $phpcsFile->findNext([T_SEMICOLON], $token['parenthesis_closer']);
8✔
1448
                        if (! is_int($nextSemicolonIndex)) {
8✔
1449
                                $nextSemicolonIndex = $token['parenthesis_closer'] + 1;
×
1450
                        }
1451
                        $blockEnd = $nextSemicolonIndex;
8✔
1452
                }
1453
                $initStart = intval($token['parenthesis_opener']) + 1;
8✔
1454
                $initEnd = null;
8✔
1455
                $conditionStart = null;
8✔
1456
                $conditionEnd = null;
8✔
1457
                $incrementStart = null;
8✔
1458
                $incrementEnd = $token['parenthesis_closer'] - 1;
8✔
1459

1460
                $semicolonCount = 0;
8✔
1461
                $forLoopLevel = $tokens[$forIndex]['level'];
8✔
1462
                $forLoopNestedParensCount = 1;
8✔
1463

1464
                if (isset($tokens[$forIndex]['nested_parenthesis'])) {
8✔
1465
                        $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
×
1466
                }
1467

1468
                for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
8✔
1469
                        if ($tokens[$i]['code'] !== T_SEMICOLON) {
8✔
1470
                                continue;
8✔
1471
                        }
1472

1473
                        if ($tokens[$i]['level'] !== $forLoopLevel) {
8✔
1474
                                continue;
8✔
1475
                        }
1476

1477
                        if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
8✔
1478
                                continue;
×
1479
                        }
1480

1481
                        switch ($semicolonCount) {
1482
                                case 0:
8✔
1483
                                        $initEnd = $i;
8✔
1484
                                        $conditionStart = $initEnd + 1;
8✔
1485
                                        break;
8✔
1486
                                case 1:
8✔
1487
                                        $conditionEnd = $i;
8✔
1488
                                        $incrementStart = $conditionEnd + 1;
8✔
1489
                                        break;
8✔
1490
                        }
1491
                        $semicolonCount += 1;
8✔
1492
                }
4✔
1493

1494
                if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
8✔
1495
                        throw new \Exception("Cannot parse for loop at position {$forIndex}");
×
1496
                }
1497

1498
                return new ForLoopInfo(
8✔
1499
                        $forIndex,
8✔
1500
                        $blockStart,
8✔
1501
                        $blockEnd,
8✔
1502
                        $initStart,
8✔
1503
                        $initEnd,
8✔
1504
                        $conditionStart,
8✔
1505
                        $conditionEnd,
8✔
1506
                        $incrementStart,
8✔
1507
                        $incrementEnd
4✔
1508
                );
8✔
1509
        }
1510

1511
        /**
1512
         * @param int                     $stackPtr
1513
         * @param array<int, ForLoopInfo> $forLoops
1514
         * @return ForLoopInfo|null
1515
         */
1516
        public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
340✔
1517
        {
1518
                foreach ($forLoops as $forLoop) {
340✔
1519
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1520
                                return $forLoop;
8✔
1521
                        }
1522
                }
170✔
1523
                return null;
340✔
1524
        }
1525

1526
        /**
1527
         * Return true if the token looks like constructor promotion.
1528
         *
1529
         * Call on a parameter variable token only.
1530
         *
1531
         * @param File $phpcsFile
1532
         * @param int  $stackPtr
1533
         *
1534
         * @return bool
1535
         */
1536
        public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
228✔
1537
        {
1538
                $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
228✔
1539
                if (! $functionIndex) {
228✔
1540
                        return false;
×
1541
                }
1542

1543
                $tokens = $phpcsFile->getTokens();
228✔
1544

1545
                // If the previous token is a visibility keyword, this is constructor
1546
                // promotion. eg: `public $foobar`.
1547
                $prevIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), $functionIndex, true);
228✔
1548
                if (! is_int($prevIndex)) {
228✔
1549
                        return false;
×
1550
                }
1551
                $prevToken = $tokens[$prevIndex];
228✔
1552
                if (in_array($prevToken['code'], Tokens::$scopeModifiers, true)) {
228✔
1553
                        return true;
8✔
1554
                }
1555

1556
                // If the previous token is not a visibility keyword, but the one before it
1557
                // is, the previous token was probably a typehint and this is constructor
1558
                // promotion. eg: `public boolean $foobar`.
1559
                $prev2Index = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevIndex - 1), $functionIndex, true);
228✔
1560
                if (! is_int($prev2Index)) {
228✔
1561
                        return false;
×
1562
                }
1563
                $prev2Token = $tokens[$prev2Index];
228✔
1564
                if (in_array($prev2Token['code'], Tokens::$scopeModifiers, true)) {
228✔
1565
                        return true;
12✔
1566
                }
1567

1568
                // If the previous token is not a visibility keyword, but the one two
1569
                // before it is, and one of the tokens is `readonly`, the previous token
1570
                // was probably a typehint and this is constructor promotion. eg: `public
1571
                // readonly boolean $foobar`.
1572
                $prev3Index = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev2Index - 1), $functionIndex, true);
224✔
1573
                if (! is_int($prev3Index)) {
224✔
1574
                        return false;
44✔
1575
                }
1576
                $prev3Token = $tokens[$prev3Index];
224✔
1577
                $wasPreviousReadonly = $prevToken['content'] === 'readonly' || $prev2Token['content'] === 'readonly';
224✔
1578
                if (in_array($prev3Token['code'], Tokens::$scopeModifiers, true) && $wasPreviousReadonly) {
224✔
1579
                        return true;
8✔
1580
                }
1581

1582
                return false;
224✔
1583
        }
1584

1585
        /**
1586
         * Return true if the token is inside an abstract class.
1587
         *
1588
         * @param File $phpcsFile
1589
         * @param int  $stackPtr
1590
         *
1591
         * @return bool
1592
         */
1593
        public static function isInAbstractClass(File $phpcsFile, $stackPtr)
108✔
1594
        {
1595
                $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
108✔
1596
                if (! is_int($classIndex)) {
108✔
1597
                        return false;
92✔
1598
                }
1599
                $classProperties = $phpcsFile->getClassProperties($classIndex);
16✔
1600
                return $classProperties['is_abstract'];
16✔
1601
        }
1602

1603
        /**
1604
         * Return true if the function body is empty or contains only `return;`
1605
         *
1606
         * @param File $phpcsFile
1607
         * @param int  $stackPtr  The index of the function keyword.
1608
         *
1609
         * @return bool
1610
         */
1611
        public static function isFunctionBodyEmpty(File $phpcsFile, $stackPtr)
8✔
1612
        {
1613
                $tokens = $phpcsFile->getTokens();
8✔
1614
                if ($tokens[$stackPtr]['code'] !== T_FUNCTION) {
8✔
1615
                        return false;
×
1616
                }
1617
                $functionScopeStart = $tokens[$stackPtr]['scope_opener'];
8✔
1618
                $functionScopeEnd = $tokens[$stackPtr]['scope_closer'];
8✔
1619
                $tokensToIgnore = array_merge(
8✔
1620
                        Tokens::$emptyTokens,
8✔
1621
                        [
4✔
1622
                                T_RETURN,
8✔
1623
                                T_SEMICOLON,
8✔
1624
                                T_OPEN_CURLY_BRACKET,
8✔
1625
                                T_CLOSE_CURLY_BRACKET,
8✔
1626
                        ]
4✔
1627
                );
8✔
1628
                for ($i = $functionScopeStart; $i < $functionScopeEnd; $i++) {
8✔
1629
                        if (! in_array($tokens[$i]['code'], $tokensToIgnore, true)) {
8✔
1630
                                return false;
8✔
1631
                        }
1632
                }
4✔
1633
                return true;
8✔
1634
        }
1635
}
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