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

sirbrillig / phpcs-variable-analysis / 4395464474

pending completion
4395464474

push

github

Payton Swick
Add enum support

49 of 50 new or added lines in 3 files covered. (98.0%)

1490 of 1614 relevant lines covered (92.32%)

138.28 hits per line

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

85.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\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()
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)
35
        {
36
                return is_int($value) ? $value : null;
340✔
37
        }
38

39
        /**
40
         * @param File $phpcsFile
41
         * @param int  $stackPtr
42
         *
43
         * @return ?int
44
         */
45
        public static function findContainingOpeningSquareBracket(File $phpcsFile, $stackPtr)
46
        {
47
                $previousStatementPtr = self::getPreviousStatementPtr($phpcsFile, $stackPtr);
324✔
48
                return self::getIntOrNull($phpcsFile->findPrevious([T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET], $stackPtr - 1, $previousStatementPtr));
324✔
49
        }
50

51
        /**
52
         * @param File $phpcsFile
53
         * @param int  $stackPtr
54
         *
55
         * @return int
56
         */
57
        public static function getPreviousStatementPtr(File $phpcsFile, $stackPtr)
58
        {
59
                $result = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET], $stackPtr - 1);
324✔
60
                return is_bool($result) ? 1 : $result;
324✔
61
        }
62

63
        /**
64
         * @param File $phpcsFile
65
         * @param int  $stackPtr
66
         *
67
         * @return ?int
68
         */
69
        public static function findContainingOpeningBracket(File $phpcsFile, $stackPtr)
70
        {
71
                $tokens = $phpcsFile->getTokens();
340✔
72
                if (isset($tokens[$stackPtr]['nested_parenthesis'])) {
340✔
73
                        /**
74
                         * @var array<int|string|null>
75
                         */
76
                        $openPtrs = array_keys($tokens[$stackPtr]['nested_parenthesis']);
256✔
77
                        return (int)end($openPtrs);
256✔
78
                }
79
                return null;
332✔
80
        }
81

82
        /**
83
         * @param array{conditions: (int|string)[], content: string} $token
84
         *
85
         * @return bool
86
         */
87
        public static function areAnyConditionsAClass(array $token)
88
        {
89
                $conditions = $token['conditions'];
28✔
90
                $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
28✔
91
                if (defined('T_ENUM')) {
28✔
92
                        $classlikeCodes[] = T_ENUM;
14✔
93
                }
7✔
94
                $classlikeCodes[] = 'PHPCS_T_ENUM';
28✔
95
                foreach (array_reverse($conditions, true) as $scopeCode) {
28✔
96
                        if (in_array($scopeCode, $classlikeCodes, true)) {
28✔
97
                                return true;
28✔
98
                        }
99
                }
14✔
100
                return false;
8✔
101
        }
102

103
        /**
104
         * Return true if the token conditions are within a function before they are
105
         * within a class.
106
         *
107
         * @param array{conditions: (int|string)[], content: string} $token
108
         *
109
         * @return bool
110
         */
111
        public static function areConditionsWithinFunctionBeforeClass(array $token)
112
        {
113
                $conditions = $token['conditions'];
48✔
114
                $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
48✔
115
                if (defined('T_ENUM')) {
48✔
116
                        $classlikeCodes[] = T_ENUM;
24✔
117
                }
12✔
118
                $classlikeCodes[] = 'PHPCS_T_ENUM';
48✔
119
                foreach (array_reverse($conditions, true) as $scopeCode) {
48✔
120
                        if (in_array($scopeCode, $classlikeCodes)) {
48✔
121
                                return false;
4✔
122
                        }
123
                        if ($scopeCode === T_FUNCTION) {
48✔
124
                                return true;
48✔
125
                        }
126
                }
12✔
127
                return false;
×
128
        }
129

130
        /**
131
         * Return true if the token conditions are within an if block before they are
132
         * within a class or function.
133
         *
134
         * @param (int|string)[] $conditions
135
         *
136
         * @return int|string|null
137
         */
138
        public static function getClosestIfPositionIfBeforeOtherConditions(array $conditions)
139
        {
140
                $conditionsInsideOut = array_reverse($conditions, true);
8✔
141
                if (empty($conditions)) {
8✔
142
                        return null;
×
143
                }
144
                $scopeCode = reset($conditionsInsideOut);
8✔
145
                if ($scopeCode === T_IF) {
8✔
146
                        return key($conditionsInsideOut);
8✔
147
                }
148
                return null;
8✔
149
        }
150

151
        /**
152
         * @param File $phpcsFile
153
         * @param int  $stackPtr
154
         *
155
         * @return bool
156
         */
157
        public static function isTokenFunctionParameter(File $phpcsFile, $stackPtr)
158
        {
159
                return is_int(self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr));
344✔
160
        }
161

162
        /**
163
         * Return true if the token is inside the arguments of a function call.
164
         *
165
         * For example, the variable `$foo` in `doSomething($foo)` is inside the
166
         * arguments to the call to `doSomething()`.
167
         *
168
         * @param File $phpcsFile
169
         * @param int  $stackPtr
170
         *
171
         * @return bool
172
         */
173
        public static function isTokenInsideFunctionCallArgument(File $phpcsFile, $stackPtr)
174
        {
175
                return is_int(self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr));
312✔
176
        }
177

178
        /**
179
         * Find the index of the function keyword for a token in a function
180
         * definition's parameters.
181
         *
182
         * Does not work for tokens inside the "use".
183
         *
184
         * Will also work for the parenthesis that make up the function definition's
185
         * parameters list.
186
         *
187
         * For arguments inside a function call, rather than a definition, use
188
         * `getFunctionIndexForFunctionCallArgument`.
189
         *
190
         * @param File $phpcsFile
191
         * @param int  $stackPtr
192
         *
193
         * @return ?int
194
         */
195
        public static function getFunctionIndexForFunctionParameter(File $phpcsFile, $stackPtr)
196
        {
197
                $tokens = $phpcsFile->getTokens();
344✔
198
                $token = $tokens[$stackPtr];
344✔
199
                if ($token['code'] === 'PHPCS_T_OPEN_PARENTHESIS') {
344✔
200
                        $startOfArguments = $stackPtr;
×
201
                } elseif ($token['code'] === 'PHPCS_T_CLOSE_PARENTHESIS') {
344✔
202
                        if (empty($token['parenthesis_opener'])) {
24✔
203
                                return null;
×
204
                        }
205
                        $startOfArguments = $token['parenthesis_opener'];
24✔
206
                } else {
12✔
207
                        if (empty($token['nested_parenthesis'])) {
344✔
208
                                return null;
336✔
209
                        }
210
                        $startingParenthesis = array_keys($token['nested_parenthesis']);
280✔
211
                        $startOfArguments = end($startingParenthesis);
280✔
212
                }
213

214
                if (! is_int($startOfArguments)) {
280✔
215
                        return null;
×
216
                }
217

218
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
280✔
219
                $nonFunctionTokenTypes[] = T_STRING;
280✔
220
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
280✔
221
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
280✔
222
                if (! is_int($functionPtr)) {
280✔
223
                        return null;
×
224
                }
225
                $functionToken = $tokens[$functionPtr];
280✔
226

227
                $functionTokenTypes = [
140✔
228
                        T_FUNCTION,
280✔
229
                        T_CLOSURE,
280✔
230
                ];
280✔
231
                if (!in_array($functionToken['code'], $functionTokenTypes, true) && ! self::isArrowFunction($phpcsFile, $functionPtr)) {
280✔
232
                        return null;
260✔
233
                }
234
                return $functionPtr;
224✔
235
        }
236

237
        /**
238
         * @param File $phpcsFile
239
         * @param int  $stackPtr
240
         *
241
         * @return bool
242
         */
243
        public static function isTokenInsideFunctionUseImport(File $phpcsFile, $stackPtr)
244
        {
245
                return is_int(self::getUseIndexForUseImport($phpcsFile, $stackPtr));
340✔
246
        }
247

248
        /**
249
         * Find the token index of the "use" for a token inside a function use import
250
         *
251
         * @param File $phpcsFile
252
         * @param int  $stackPtr
253
         *
254
         * @return ?int
255
         */
256
        public static function getUseIndexForUseImport(File $phpcsFile, $stackPtr)
257
        {
258
                $tokens = $phpcsFile->getTokens();
340✔
259

260
                $nonUseTokenTypes = Tokens::$emptyTokens;
340✔
261
                $nonUseTokenTypes[] = T_VARIABLE;
340✔
262
                $nonUseTokenTypes[] = T_ELLIPSIS;
340✔
263
                $nonUseTokenTypes[] = T_COMMA;
340✔
264
                $nonUseTokenTypes[] = T_BITWISE_AND;
340✔
265
                $openParenPtr = self::getIntOrNull($phpcsFile->findPrevious($nonUseTokenTypes, $stackPtr - 1, null, true, null, true));
340✔
266
                if (! is_int($openParenPtr) || $tokens[$openParenPtr]['code'] !== T_OPEN_PARENTHESIS) {
340✔
267
                        return null;
332✔
268
                }
269

270
                $usePtr = self::getIntOrNull($phpcsFile->findPrevious(array_values($nonUseTokenTypes), $openParenPtr - 1, null, true, null, true));
212✔
271
                if (! is_int($usePtr) || $tokens[$usePtr]['code'] !== T_USE) {
212✔
272
                        return null;
200✔
273
                }
274
                return $usePtr;
24✔
275
        }
276

277
        /**
278
         * Return the index of a function's name token from inside the function.
279
         *
280
         * $stackPtr must be inside the function body or parameters for this to work.
281
         *
282
         * @param File $phpcsFile
283
         * @param int  $stackPtr
284
         *
285
         * @return ?int
286
         */
287
        public static function findFunctionCall(File $phpcsFile, $stackPtr)
288
        {
289
                $tokens = $phpcsFile->getTokens();
320✔
290

291
                $openPtr = self::findContainingOpeningBracket($phpcsFile, $stackPtr);
320✔
292
                if (is_int($openPtr)) {
320✔
293
                        // First non-whitespace thing and see if it's a T_STRING function name
294
                        $functionPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
208✔
295
                        if (is_int($functionPtr) && $tokens[$functionPtr]['code'] === T_STRING) {
208✔
296
                                return $functionPtr;
154✔
297
                        }
298
                }
67✔
299
                return null;
306✔
300
        }
301

302
        /**
303
         * @param File $phpcsFile
304
         * @param int  $stackPtr
305
         *
306
         * @return array<int, array<int>>
307
         */
308
        public static function findFunctionCallArguments(File $phpcsFile, $stackPtr)
309
        {
310
                $tokens = $phpcsFile->getTokens();
32✔
311

312
                // Slight hack: also allow this to find args for array constructor.
313
                if (($tokens[$stackPtr]['code'] !== T_STRING) && ($tokens[$stackPtr]['code'] !== T_ARRAY)) {
32✔
314
                        // Assume $stackPtr is something within the brackets, find our function call
315
                        $stackPtr = self::findFunctionCall($phpcsFile, $stackPtr);
20✔
316
                        if ($stackPtr === null) {
20✔
317
                                return [];
×
318
                        }
319
                }
10✔
320

321
                // $stackPtr is the function name, find our brackets after it
322
                $openPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
32✔
323
                if (($openPtr === false) || ($tokens[$openPtr]['code'] !== T_OPEN_PARENTHESIS)) {
32✔
324
                        return [];
×
325
                }
326

327
                if (!isset($tokens[$openPtr]['parenthesis_closer'])) {
32✔
328
                        return [];
×
329
                }
330
                $closePtr = $tokens[$openPtr]['parenthesis_closer'];
32✔
331

332
                $argPtrs = [];
32✔
333
                $lastPtr = $openPtr;
32✔
334
                $lastArgComma = $openPtr;
32✔
335
                $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
32✔
336
                while (is_int($nextPtr)) {
32✔
337
                        if (self::findContainingOpeningBracket($phpcsFile, $nextPtr) === $openPtr) {
32✔
338
                                // Comma is at our level of brackets, it's an argument delimiter.
339
                                $range = range($lastArgComma + 1, $nextPtr - 1);
32✔
340
                                $range = array_filter($range, function ($element) {
16✔
341
                                        return is_int($element);
32✔
342
                                });
32✔
343
                                array_push($argPtrs, $range);
32✔
344
                                $lastArgComma = $nextPtr;
32✔
345
                        }
16✔
346
                        $lastPtr = $nextPtr;
32✔
347
                        $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
32✔
348
                }
16✔
349
                $range = range($lastArgComma + 1, $closePtr - 1);
32✔
350
                $range = array_filter($range, function ($element) {
16✔
351
                        return is_int($element);
32✔
352
                });
32✔
353
                array_push($argPtrs, $range);
32✔
354

355
                return $argPtrs;
32✔
356
        }
357

358
        /**
359
         * @param File $phpcsFile
360
         * @param int  $stackPtr
361
         *
362
         * @return ?int
363
         */
364
        public static function getNextAssignPointer(File $phpcsFile, $stackPtr)
365
        {
366
                $tokens = $phpcsFile->getTokens();
332✔
367

368
                // Is the next non-whitespace an assignment?
369
                $nextPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
332✔
370
                if (is_int($nextPtr)
332✔
371
                        && isset(Tokens::$assignmentTokens[$tokens[$nextPtr]['code']])
332✔
372
                        // Ignore double arrow to prevent triggering on `foreach ( $array as $k => $v )`.
373
                        && $tokens[$nextPtr]['code'] !== T_DOUBLE_ARROW
332✔
374
                ) {
166✔
375
                        return $nextPtr;
312✔
376
                }
377
                return null;
324✔
378
        }
379

380
        /**
381
         * @param string $varName
382
         *
383
         * @return string
384
         */
385
        public static function normalizeVarName($varName)
386
        {
387
                $result = preg_replace('/[{}$]/', '', $varName);
344✔
388
                return $result ? $result : $varName;
344✔
389
        }
390

391
        /**
392
         * @param File   $phpcsFile
393
         * @param int    $stackPtr
394
         * @param string $varName   (optional) if it differs from the normalized 'content' of the token at $stackPtr
395
         *
396
         * @return ?int
397
         */
398
        public static function findVariableScope(File $phpcsFile, $stackPtr, $varName = null)
399
        {
400
                $tokens = $phpcsFile->getTokens();
344✔
401
                $token = $tokens[$stackPtr];
344✔
402
                $varName = isset($varName) ? $varName : self::normalizeVarName($token['content']);
344✔
403

404
                $arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr);
344✔
405
                $isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex);
344✔
406
                if ($isTokenInsideArrowFunctionBody) {
344✔
407
                        // Get the list of variables defined by the arrow function
408
                        // If this matches any of them, the scope is the arrow function,
409
                        // otherwise, it uses the enclosing scope.
410
                        if ($arrowFunctionIndex) {
24✔
411
                                $variableNames = self::getVariablesDefinedByArrowFunction($phpcsFile, $arrowFunctionIndex);
24✔
412
                                self::debug('findVariableScope: looking for', $varName, 'in arrow function variables', $variableNames);
24✔
413
                                if (in_array($varName, $variableNames, true)) {
24✔
414
                                        return $arrowFunctionIndex;
24✔
415
                                }
416
                        }
8✔
417
                }
8✔
418

419
                return self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
344✔
420
        }
421

422
        /**
423
         * Return the token index of the scope start for a token
424
         *
425
         * For a variable within a function body, or a variable within a function
426
         * definition argument list, this will return the function keyword's index.
427
         *
428
         * For a variable within a "use" import list within a function definition,
429
         * this will return the enclosing scope, not the function keyword. This is
430
         * important to note because the "use" keyword performs double-duty, defining
431
         * variables for the function's scope, and consuming the variables in the
432
         * enclosing scope. Use `getUseIndexForUseImport` to determine if this
433
         * token needs to be treated as a "use".
434
         *
435
         * For a variable within an arrow function definition argument list,
436
         * this will return the arrow function's keyword index.
437
         *
438
         * For a variable in an arrow function body, this will return the enclosing
439
         * function's index, which may be incorrect.
440
         *
441
         * Since a variable in an arrow function's body may be imported from the
442
         * enclosing scope, it's important to test to see if the variable is in an
443
         * arrow function and also check its enclosing scope separately.
444
         *
445
         * @param File $phpcsFile
446
         * @param int  $stackPtr
447
         *
448
         * @return ?int
449
         */
450
        public static function findVariableScopeExceptArrowFunctions(File $phpcsFile, $stackPtr)
451
        {
452
                $tokens = $phpcsFile->getTokens();
344✔
453
                $allowedTypes = [
172✔
454
                        T_VARIABLE,
344✔
455
                        T_DOUBLE_QUOTED_STRING,
344✔
456
                        T_HEREDOC,
344✔
457
                        T_STRING,
344✔
458
                ];
344✔
459
                if (! in_array($tokens[$stackPtr]['code'], $allowedTypes, true)) {
344✔
460
                        throw new \Exception("Cannot find variable scope for non-variable {$tokens[$stackPtr]['type']}");
×
461
                }
462

463
                $startOfTokenScope = self::getStartOfTokenScope($phpcsFile, $stackPtr);
344✔
464
                if (is_int($startOfTokenScope) && $startOfTokenScope > 0) {
344✔
465
                        return $startOfTokenScope;
332✔
466
                }
467

468
                // If there is no "conditions" array, this is a function definition argument.
469
                if (self::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
272✔
470
                        $functionPtr = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
220✔
471
                        if (! is_int($functionPtr)) {
220✔
472
                                throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
473
                        }
474
                        return $functionPtr;
220✔
475
                }
476

477
                self::debug('Cannot find function scope for variable at', $stackPtr);
108✔
478
                return $startOfTokenScope;
108✔
479
        }
480

481
        /**
482
         * Return the token index of the scope start for a variable token
483
         *
484
         * This will only work for a variable within a function's body. Otherwise,
485
         * see `findVariableScope`, which is more complex.
486
         *
487
         * Note that if used on a variable in an arrow function, it will return the
488
         * enclosing function's scope, which may be incorrect.
489
         *
490
         * @param File $phpcsFile
491
         * @param int  $stackPtr
492
         *
493
         * @return ?int
494
         */
495
        private static function getStartOfTokenScope(File $phpcsFile, $stackPtr)
496
        {
497
                $tokens = $phpcsFile->getTokens();
344✔
498
                $token = $tokens[$stackPtr];
344✔
499

500
                $inClass = false;
344✔
501
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
344✔
502
                $functionTokenTypes = [
172✔
503
                        T_FUNCTION,
344✔
504
                        T_CLOSURE,
344✔
505
                ];
344✔
506
                foreach (array_reverse($conditions, true) as $scopePtr => $scopeCode) {
344✔
507
                        if (in_array($scopeCode, $functionTokenTypes, true) || self::isArrowFunction($phpcsFile, $scopePtr)) {
336✔
508
                                return $scopePtr;
332✔
509
                        }
510
                        if (isset(Tokens::$ooScopeTokens[$scopeCode]) === true) {
148✔
511
                                $inClass = true;
56✔
512
                        }
28✔
513
                }
154✔
514

515
                if ($inClass) {
272✔
516
                        // If this is inside a class and not inside a function, this is either a
517
                        // class member variable definition, or a function argument. If it is a
518
                        // variable definition, it has no scope on its own (it can only be used
519
                        // with an object reference). If it is a function argument, we need to do
520
                        // more work (see `findVariableScopeExceptArrowFunctions`).
521
                        return null;
56✔
522
                }
523

524
                // If we can't find a scope, let's use the first token of the file.
525
                return 0;
236✔
526
        }
527

528
        /**
529
         * @param File $phpcsFile
530
         * @param int  $stackPtr
531
         *
532
         * @return bool
533
         */
534
        public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $stackPtr)
535
        {
536
                $tokens = $phpcsFile->getTokens();
×
537
                $token = $tokens[$stackPtr];
×
538
                $openParenIndices = isset($token['nested_parenthesis']) ? $token['nested_parenthesis'] : [];
×
539
                if (empty($openParenIndices)) {
×
540
                        return false;
×
541
                }
542
                $openParenPtr = $openParenIndices[0];
×
543
                return self::isArrowFunction($phpcsFile, $openParenPtr - 1);
×
544
        }
545

546
        /**
547
         * @param File $phpcsFile
548
         * @param int  $stackPtr
549
         *
550
         * @return ?int
551
         */
552
        public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr)
553
        {
554
                $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr);
344✔
555
                if (! is_int($arrowFunctionIndex)) {
344✔
556
                        return null;
344✔
557
                }
558
                $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex);
24✔
559
                if (! $arrowFunctionInfo) {
24✔
560
                        return null;
×
561
                }
562
                $arrowFunctionScopeStart = $arrowFunctionInfo['scope_opener'];
24✔
563
                $arrowFunctionScopeEnd = $arrowFunctionInfo['scope_closer'];
24✔
564
                if ($stackPtr > $arrowFunctionScopeStart && $stackPtr < $arrowFunctionScopeEnd) {
24✔
565
                        return $arrowFunctionIndex;
24✔
566
                }
567
                return null;
24✔
568
        }
569

570
        /**
571
         * @param File $phpcsFile
572
         * @param int  $stackPtr
573
         *
574
         * @return ?int
575
         */
576
        private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr)
577
        {
578
                $tokens = $phpcsFile->getTokens();
344✔
579
                $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
344✔
580
                for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) {
344✔
581
                        $token = $tokens[$index];
344✔
582
                        if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) {
344✔
583
                                return $index;
24✔
584
                        }
585
                }
172✔
586
                return null;
344✔
587
        }
588

589
        /**
590
         * @param File $phpcsFile
591
         * @param int  $stackPtr
592
         *
593
         * @return bool
594
         */
595
        public static function isArrowFunction(File $phpcsFile, $stackPtr)
596
        {
597
                $tokens = $phpcsFile->getTokens();
344✔
598
                if (defined('T_FN') && $tokens[$stackPtr]['code'] === T_FN) {
344✔
599
                        return true;
24✔
600
                }
601
                if ($tokens[$stackPtr]['content'] !== 'fn') {
344✔
602
                        return false;
344✔
603
                }
604
                // Make sure next non-space token is an open parenthesis
605
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
×
606
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
×
607
                        return false;
×
608
                }
609
                // Find the associated close parenthesis
610
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
×
611
                // Make sure the next token is a fat arrow
612
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
×
613
                if (! is_int($fatArrowIndex)) {
×
614
                        return false;
×
615
                }
616
                if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') {
×
617
                        return false;
×
618
                }
619
                return true;
×
620
        }
621

622
        /**
623
         * @param File $phpcsFile
624
         * @param int  $stackPtr
625
         *
626
         * @return ?array<string, int>
627
         */
628
        public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
629
        {
630
                $tokens = $phpcsFile->getTokens();
24✔
631
                if (defined('T_FN') && $tokens[$stackPtr]['code'] === T_FN) {
24✔
632
                        return [
12✔
633
                                'scope_opener' => $tokens[$stackPtr]['scope_opener'],
24✔
634
                                'scope_closer' => $tokens[$stackPtr]['scope_closer'],
24✔
635
                        ];
24✔
636
                }
637
                if ($tokens[$stackPtr]['content'] !== 'fn') {
×
638
                        return null;
×
639
                }
640
                // Make sure next non-space token is an open parenthesis
641
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
×
642
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
×
643
                        return null;
×
644
                }
645
                // Find the associated close parenthesis
646
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
×
647
                // Make sure the next token is a fat arrow
648
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
×
649
                if (! is_int($fatArrowIndex)) {
×
650
                        return null;
×
651
                }
652
                if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') {
×
653
                        return null;
×
654
                }
655
                // Find the scope closer
656
                $endScopeTokens = [
657
                        T_COMMA,
×
658
                        T_SEMICOLON,
×
659
                        T_CLOSE_PARENTHESIS,
×
660
                        T_CLOSE_CURLY_BRACKET,
×
661
                        T_CLOSE_SHORT_ARRAY,
×
662
                ];
×
663
                $scopeCloserIndex = $phpcsFile->findNext($endScopeTokens, $fatArrowIndex        + 1);
×
664
                if (! is_int($scopeCloserIndex)) {
×
665
                        return null;
×
666
                }
667
                return [
668
                        'scope_opener' => $stackPtr,
×
669
                        'scope_closer' => $scopeCloserIndex,
×
670
                ];
×
671
        }
672

673
        /**
674
         * Determine if a token is a list opener for list assignment/destructuring.
675
         *
676
         * The index provided can be either the opening square brace of a short list
677
         * assignment like the first character of `[$a] = $b;` or the `list` token of
678
         * an expression like `list($a) = $b;` or the opening parenthesis of that
679
         * expression.
680
         *
681
         * @param File $phpcsFile
682
         * @param int  $listOpenerIndex
683
         *
684
         * @return bool
685
         */
686
        private static function isListAssignment(File $phpcsFile, $listOpenerIndex)
687
        {
688
                $tokens = $phpcsFile->getTokens();
44✔
689
                // Match `[$a] = $b;` except for when the previous token is a parenthesis.
690
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SHORT_ARRAY) {
44✔
691
                        return true;
28✔
692
                }
693
                // Match `list($a) = $b;`
694
                if ($tokens[$listOpenerIndex]['code'] === T_LIST) {
44✔
695
                        return true;
32✔
696
                }
697

698
                // If $listOpenerIndex is the open parenthesis of `list($a) = $b;`, then
699
                // match that too.
700
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_PARENTHESIS) {
32✔
701
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
4✔
702
                        if (
703
                                isset($tokens[$previousTokenPtr])
4✔
704
                                && $tokens[$previousTokenPtr]['code'] === T_LIST
4✔
705
                        ) {
2✔
706
                                return true;
4✔
707
                        }
708
                        return true;
×
709
                }
710

711
                // If the list opener token is a square bracket that is preceeded by a
712
                // close parenthesis that has an owner which is a scope opener, then this
713
                // is a list assignment and not an array access.
714
                //
715
                // Match `if (true) [$a] = $b;`
716
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SQUARE_BRACKET) {
28✔
717
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
28✔
718
                        if (
719
                                isset($tokens[$previousTokenPtr])
28✔
720
                                && $tokens[$previousTokenPtr]['code'] === T_CLOSE_PARENTHESIS
28✔
721
                                && isset($tokens[$previousTokenPtr]['parenthesis_owner'])
28✔
722
                                && isset(Tokens::$scopeOpeners[$tokens[$tokens[$previousTokenPtr]['parenthesis_owner']]['code']])
28✔
723
                        ) {
14✔
724
                                return true;
4✔
725
                        }
726
                }
14✔
727

728
                return false;
28✔
729
        }
730

731
        /**
732
         * Return a list of indices for variables assigned within a list assignment.
733
         *
734
         * The index provided can be either the opening square brace of a short list
735
         * assignment like the first character of `[$a] = $b;` or the `list` token of
736
         * an expression like `list($a) = $b;` or the opening parenthesis of that
737
         * expression.
738
         *
739
         * @param File $phpcsFile
740
         * @param int  $listOpenerIndex
741
         *
742
         * @return ?array<int>
743
         */
744
        public static function getListAssignments(File $phpcsFile, $listOpenerIndex)
745
        {
746
                $tokens = $phpcsFile->getTokens();
52✔
747
                self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]);
52✔
748

749
                // First find the end of the list
750
                $closePtr = null;
52✔
751
                if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) {
52✔
752
                        $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer'];
32✔
753
                }
16✔
754
                if (isset($tokens[$listOpenerIndex]['bracket_closer'])) {
52✔
755
                        $closePtr = $tokens[$listOpenerIndex]['bracket_closer'];
52✔
756
                }
26✔
757
                if (! $closePtr) {
52✔
758
                        return null;
×
759
                }
760

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

764
                // If the next token isn't an assignment, check for nested brackets because we might be a nested assignment
765
                if (! is_int($assignPtr) || $tokens[$assignPtr]['code'] !== T_EQUAL) {
52✔
766
                        // Collect the enclosing list open/close tokens ($parents is an assoc array keyed by opener index and the value is the closer index)
767
                        $parents = isset($tokens[$listOpenerIndex]['nested_parenthesis']) ? $tokens[$listOpenerIndex]['nested_parenthesis'] : [];
32✔
768
                        // There's no record of nested brackets for short lists; we'll have to find the parent ourselves
769
                        if (empty($parents)) {
32✔
770
                                $parentSquareBracket = self::findContainingOpeningSquareBracket($phpcsFile, $listOpenerIndex);
32✔
771
                                if (is_int($parentSquareBracket)) {
32✔
772
                                        // Collect the opening index, but we don't actually need the closing paren index so just make that 0
773
                                        $parents = [$parentSquareBracket => 0];
4✔
774
                                }
2✔
775
                        }
16✔
776
                        // If we have no parents, this is not a nested assignment and therefore is not an assignment
777
                        if (empty($parents)) {
32✔
778
                                return null;
28✔
779
                        }
780

781
                        // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion)
782
                        $isNestedAssignment = null;
12✔
783
                        $parentListOpener = array_keys(array_reverse($parents, true))[0];
12✔
784
                        $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener);
12✔
785
                        if ($isNestedAssignment === null) {
12✔
786
                                return null;
8✔
787
                        }
788
                }
2✔
789

790
                $variablePtrs = [];
44✔
791

792
                $currentPtr = $listOpenerIndex;
44✔
793
                $variablePtr = 0;
44✔
794
                while ($currentPtr < $closePtr && is_int($variablePtr)) {
44✔
795
                        $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr);
44✔
796
                        if (is_int($variablePtr)) {
44✔
797
                                $variablePtrs[] = $variablePtr;
32✔
798
                        }
16✔
799
                        ++$currentPtr;
44✔
800
                }
22✔
801

802
                if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) {
44✔
803
                        return null;
28✔
804
                }
805

806
                return $variablePtrs;
32✔
807
        }
808

809
        /**
810
         * @param File $phpcsFile
811
         * @param int  $stackPtr
812
         *
813
         * @return string[]
814
         */
815
        public static function getVariablesDefinedByArrowFunction(File $phpcsFile, $stackPtr)
816
        {
817
                $tokens = $phpcsFile->getTokens();
24✔
818
                $arrowFunctionToken = $tokens[$stackPtr];
24✔
819
                $variableNames = [];
24✔
820
                self::debug('looking for variables in arrow function token', $arrowFunctionToken);
24✔
821
                for ($index = $arrowFunctionToken['parenthesis_opener']; $index < $arrowFunctionToken['parenthesis_closer']; $index++) {
24✔
822
                        $token = $tokens[$index];
24✔
823
                        if ($token['code'] === T_VARIABLE) {
24✔
824
                                $variableNames[] = self::normalizeVarName($token['content']);
24✔
825
                        }
12✔
826
                }
12✔
827
                self::debug('found these variables in arrow function token', $variableNames);
24✔
828
                return $variableNames;
24✔
829
        }
830

831
        /**
832
         * @return void
833
         */
834
        public static function debug()
835
        {
836
                $messages = func_get_args();
344✔
837
                if (! defined('PHP_CODESNIFFER_VERBOSITY')) {
344✔
838
                        return;
×
839
                }
840
                if (PHP_CODESNIFFER_VERBOSITY <= 3) {
344✔
841
                        return;
344✔
842
                }
843
                $output = PHP_EOL . 'VariableAnalysisSniff: DEBUG:';
×
844
                foreach ($messages as $message) {
×
845
                        if (is_string($message) || is_numeric($message)) {
×
846
                                $output .= ' "' . $message . '"';
×
847
                                continue;
×
848
                        }
849
                        $output .= PHP_EOL . var_export($message, true) . PHP_EOL;
×
850
                }
851
                $output .= PHP_EOL;
×
852
                echo $output;
×
853
        }
854

855
        /**
856
         * @param string $pattern
857
         * @param string $value
858
         *
859
         * @return string[]
860
         */
861
        public static function splitStringToArray($pattern, $value)
862
        {
863
                $result = preg_split($pattern, $value);
24✔
864
                return is_array($result) ? $result : [];
24✔
865
        }
866

867
        /**
868
         * @param string $varName
869
         *
870
         * @return bool
871
         */
872
        public static function isVariableANumericVariable($varName)
873
        {
874
                return is_numeric(substr($varName, 0, 1));
328✔
875
        }
876

877
        /**
878
         * @param File $phpcsFile
879
         * @param int  $stackPtr
880
         *
881
         * @return bool
882
         */
883
        public static function isVariableInsideElseCondition(File $phpcsFile, $stackPtr)
884
        {
885
                $tokens = $phpcsFile->getTokens();
320✔
886
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
320✔
887
                $nonFunctionTokenTypes[] = T_OPEN_PARENTHESIS;
320✔
888
                $nonFunctionTokenTypes[] = T_INLINE_HTML;
320✔
889
                $nonFunctionTokenTypes[] = T_CLOSE_TAG;
320✔
890
                $nonFunctionTokenTypes[] = T_VARIABLE;
320✔
891
                $nonFunctionTokenTypes[] = T_ELLIPSIS;
320✔
892
                $nonFunctionTokenTypes[] = T_COMMA;
320✔
893
                $nonFunctionTokenTypes[] = T_STRING;
320✔
894
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
320✔
895
                $elsePtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $stackPtr - 1, null, true, null, true));
320✔
896
                $elseTokenTypes = [
160✔
897
                        T_ELSE,
320✔
898
                        T_ELSEIF,
320✔
899
                ];
320✔
900
                if (is_int($elsePtr) && in_array($tokens[$elsePtr]['code'], $elseTokenTypes, true)) {
320✔
901
                        return true;
16✔
902
                }
903
                return false;
320✔
904
        }
905

906
        /**
907
         * @param File $phpcsFile
908
         * @param int  $stackPtr
909
         *
910
         * @return bool
911
         */
912
        public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr)
913
        {
914
                $tokens = $phpcsFile->getTokens();
320✔
915
                $token = $tokens[$stackPtr];
320✔
916
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
320✔
917
                $elseTokenTypes = [
160✔
918
                        T_ELSE,
320✔
919
                        T_ELSEIF,
320✔
920
                ];
320✔
921
                foreach (array_reverse($conditions, true) as $scopeCode) {
320✔
922
                        if (in_array($scopeCode, $elseTokenTypes, true)) {
308✔
923
                                return true;
8✔
924
                        }
925
                }
160✔
926

927
                // Some else body code will not have conditions because it is inline (no
928
                // curly braces) so we have to look in other ways.
929
                $previousSemicolonPtr = $phpcsFile->findPrevious([T_SEMICOLON], $stackPtr - 1);
320✔
930
                if (! is_int($previousSemicolonPtr)) {
320✔
931
                        $previousSemicolonPtr = 0;
148✔
932
                }
74✔
933
                $elsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1, $previousSemicolonPtr);
320✔
934
                if (is_int($elsePtr)) {
320✔
935
                        return true;
8✔
936
                }
937

938
                return false;
320✔
939
        }
940

941
        /**
942
         * @param File $phpcsFile
943
         * @param int  $stackPtr
944
         *
945
         * @return int[]
946
         */
947
        public static function getAttachedBlockIndicesForElse(File $phpcsFile, $stackPtr)
948
        {
949
                $currentElsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1);
16✔
950
                if (! is_int($currentElsePtr)) {
16✔
951
                        throw new \Exception("Cannot find expected else at {$stackPtr}");
×
952
                }
953

954
                $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
16✔
955
                if (! is_int($ifPtr)) {
16✔
956
                        throw new \Exception("Cannot find if for else at {$stackPtr}");
×
957
                }
958
                $blockIndices = [$ifPtr];
16✔
959

960
                $previousElseIfPtr = $currentElsePtr;
16✔
961
                do {
962
                        $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
16✔
963
                        if (is_int($elseIfPtr)) {
16✔
964
                                $blockIndices[] = $elseIfPtr;
16✔
965
                                $previousElseIfPtr = $elseIfPtr;
16✔
966
                        }
8✔
967
                } while (is_int($elseIfPtr));
16✔
968

969
                return $blockIndices;
16✔
970
        }
971

972
        /**
973
         * @param int $needle
974
         * @param int $scopeStart
975
         * @param int $scopeEnd
976
         *
977
         * @return bool
978
         */
979
        public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
980
        {
981
                return ($needle > $scopeStart && $needle < $scopeEnd);
16✔
982
        }
983

984
        /**
985
         * @param File $phpcsFile
986
         * @param int  $scopeStartIndex
987
         *
988
         * @return int
989
         */
990
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
991
        {
992
                $tokens = $phpcsFile->getTokens();
344✔
993
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
344✔
994

995
                if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) {
344✔
996
                        $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex);
24✔
997
                        $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex;
24✔
998
                }
12✔
999

1000
                if ($scopeStartIndex === 0) {
344✔
1001
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
344✔
1002
                }
172✔
1003
                return $scopeCloserIndex;
344✔
1004
        }
1005

1006
        /**
1007
         * @param File $phpcsFile
1008
         *
1009
         * @return int
1010
         */
1011
        public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
1012
        {
1013
                $tokens = $phpcsFile->getTokens();
344✔
1014
                foreach (array_reverse($tokens, true) as $index => $token) {
344✔
1015
                        if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
344✔
1016
                                return $index;
344✔
1017
                        }
1018
                }
168✔
1019
                self::debug('no non-empty token found for end of file');
×
1020
                return 0;
×
1021
        }
1022

1023
        /**
1024
         * @param VariableInfo $varInfo
1025
         * @param ScopeInfo    $scopeInfo
1026
         *
1027
         * @return bool
1028
         */
1029
        public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo)
1030
        {
1031
                $foundVarPosition = false;
64✔
1032
                foreach ($scopeInfo->variables as $variable) {
64✔
1033
                        if ($variable === $varInfo) {
64✔
1034
                                $foundVarPosition = true;
64✔
1035
                                continue;
64✔
1036
                        }
1037
                        if (! $foundVarPosition) {
44✔
1038
                                continue;
36✔
1039
                        }
1040
                        if ($variable->scopeType !== ScopeType::PARAM) {
44✔
1041
                                continue;
36✔
1042
                        }
1043
                        if ($variable->firstRead) {
16✔
1044
                                return true;
16✔
1045
                        }
1046
                }
32✔
1047
                return false;
64✔
1048
        }
1049

1050
        /**
1051
         * @param File         $phpcsFile
1052
         * @param VariableInfo $varInfo
1053
         * @param ScopeInfo    $scopeInfo
1054
         *
1055
         * @return bool
1056
         */
1057
        public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
1058
        {
1059
                $requireTokens = [
2✔
1060
                        T_REQUIRE,
4✔
1061
                        T_REQUIRE_ONCE,
4✔
1062
                        T_INCLUDE,
4✔
1063
                        T_INCLUDE_ONCE,
4✔
1064
                ];
4✔
1065
                $indexToStartSearch = $varInfo->firstDeclared;
4✔
1066
                if (! empty($varInfo->firstInitialized)) {
4✔
1067
                        $indexToStartSearch = $varInfo->firstInitialized;
4✔
1068
                }
2✔
1069
                $tokens = $phpcsFile->getTokens();
4✔
1070
                $indexToStopSearch = isset($tokens[$scopeInfo->scopeStartIndex]['scope_closer']) ? $tokens[$scopeInfo->scopeStartIndex]['scope_closer'] : null;
4✔
1071
                if (! is_int($indexToStartSearch) || ! is_int($indexToStopSearch)) {
4✔
1072
                        return false;
×
1073
                }
1074
                $requireTokenIndex = $phpcsFile->findNext($requireTokens, $indexToStartSearch + 1, $indexToStopSearch);
4✔
1075
                if (is_int($requireTokenIndex)) {
4✔
1076
                        return true;
4✔
1077
                }
1078
                return false;
×
1079
        }
1080

1081
        /**
1082
         * Find the index of the function keyword for a token in a function call's arguments
1083
         *
1084
         * For the variable `$foo` in the expression `doSomething($foo)`, this will
1085
         * return the index of the `doSomething` token.
1086
         *
1087
         * @param File $phpcsFile
1088
         * @param int  $stackPtr
1089
         *
1090
         * @return ?int
1091
         */
1092
        public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr)
1093
        {
1094
                $tokens = $phpcsFile->getTokens();
332✔
1095
                $token = $tokens[$stackPtr];
332✔
1096
                if (empty($token['nested_parenthesis'])) {
332✔
1097
                        return null;
324✔
1098
                }
1099
                /**
1100
                 * @var array<int|string|null>
1101
                 */
1102
                $startingParenthesis = array_keys($token['nested_parenthesis']);
208✔
1103
                $startOfArguments = end($startingParenthesis);
208✔
1104
                if (! is_int($startOfArguments)) {
208✔
1105
                        return null;
×
1106
                }
1107

1108
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
208✔
1109
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
208✔
1110
                if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
208✔
1111
                        return null;
×
1112
                }
1113
                if (
1114
                        $tokens[$functionPtr]['content'] === 'function'
208✔
1115
                        || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))
208✔
1116
                ) {
104✔
1117
                        // If there is a function/fn keyword before the beginning of the parens,
1118
                        // this is a function definition and not a function call.
1119
                        return null;
×
1120
                }
1121
                if (! empty($tokens[$functionPtr]['scope_opener'])) {
208✔
1122
                        // If the alleged function name has a scope, this is not a function call.
1123
                        return null;
130✔
1124
                }
1125

1126
                $functionNameType = $tokens[$functionPtr]['code'];
166✔
1127
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
166✔
1128
                        // If the alleged function name is not a variable or a string, this is
1129
                        // not a function call.
1130
                        return null;
48✔
1131
                }
1132

1133
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
162✔
1134
                        // If the variable is inside a different scope than the function name,
1135
                        // the function call doesn't apply to the variable.
1136
                        return null;
28✔
1137
                }
1138

1139
                return $functionPtr;
162✔
1140
        }
1141

1142
        /**
1143
         * @param File $phpcsFile
1144
         * @param int  $stackPtr
1145
         *
1146
         * @return bool
1147
         */
1148
        public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
1149
        {
1150
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
320✔
1151
                if (! is_int($functionIndex)) {
320✔
1152
                        return false;
302✔
1153
                }
1154
                $tokens = $phpcsFile->getTokens();
162✔
1155
                if (! isset($tokens[$functionIndex])) {
162✔
1156
                        return false;
×
1157
                }
1158
                $allowedFunctionNames = [
81✔
1159
                        'isset',
162✔
1160
                        'empty',
162✔
1161
                ];
162✔
1162
                if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) {
162✔
1163
                        return true;
4✔
1164
                }
1165
                return false;
162✔
1166
        }
1167

1168
        /**
1169
         * @param File $phpcsFile
1170
         * @param int  $stackPtr
1171
         *
1172
         * @return bool
1173
         */
1174
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
1175
        {
1176
                $tokens = $phpcsFile->getTokens();
248✔
1177
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
248✔
1178

1179
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
248✔
1180
                if (! is_int($arrayPushOperatorIndex1)) {
248✔
1181
                        return false;
×
1182
                }
1183
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
248✔
1184
                        return false;
248✔
1185
                }
1186

1187
                $arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true));
36✔
1188
                if (! is_int($arrayPushOperatorIndex2)) {
36✔
1189
                        return false;
×
1190
                }
1191
                if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') {
36✔
1192
                        return false;
8✔
1193
                }
1194

1195
                $arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true));
28✔
1196
                if (! is_int($arrayPushOperatorIndex3)) {
28✔
1197
                        return false;
×
1198
                }
1199
                if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') {
28✔
1200
                        return false;
×
1201
                }
1202

1203
                return true;
28✔
1204
        }
1205

1206
        /**
1207
         * @param File $phpcsFile
1208
         * @param int  $stackPtr
1209
         *
1210
         * @return bool
1211
         */
1212
        public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
1213
        {
1214
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
248✔
1215
                if (! is_int($functionIndex)) {
248✔
1216
                        return false;
218✔
1217
                }
1218
                $tokens = $phpcsFile->getTokens();
98✔
1219
                if (! isset($tokens[$functionIndex])) {
98✔
1220
                        return false;
×
1221
                }
1222
                if ($tokens[$functionIndex]['content'] === 'unset') {
98✔
1223
                        return true;
8✔
1224
                }
1225
                return false;
90✔
1226
        }
1227

1228
        /**
1229
         * @param File $phpcsFile
1230
         * @param int  $stackPtr
1231
         *
1232
         * @return bool
1233
         */
1234
        public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr)
1235
        {
1236
                $previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
312✔
1237
                if (! is_int($previousStatementPtr)) {
312✔
1238
                        $previousStatementPtr = 1;
32✔
1239
                }
16✔
1240
                $previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
312✔
1241
                if (is_int($previousTokenPtr)) {
312✔
1242
                        return true;
4✔
1243
                }
1244
                return false;
312✔
1245
        }
1246

1247
        /**
1248
         * @param File $phpcsFile
1249
         * @param int  $stackPtr
1250
         *
1251
         * @return bool
1252
         */
1253
        public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
1254
        {
1255
                // Is the next non-whitespace an assignment?
1256
                $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
332✔
1257
                if (! is_int($assignPtr)) {
332✔
1258
                        return false;
324✔
1259
                }
1260

1261
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1262
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
312✔
1263
                        self::debug('found variable variable');
4✔
1264
                        return false;
4✔
1265
                }
1266
                return true;
312✔
1267
        }
1268

1269
        /**
1270
         * @param File $phpcsFile
1271
         * @param int  $stackPtr
1272
         *
1273
         * @return bool
1274
         */
1275
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
1276
        {
1277
                $tokens = $phpcsFile->getTokens();
312✔
1278

1279
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
312✔
1280
                if ($prev === false) {
312✔
1281
                        return false;
×
1282
                }
1283
                if ($tokens[$prev]['code'] === T_DOLLAR) {
312✔
1284
                        return true;
4✔
1285
                }
1286
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
312✔
1287
                        return false;
256✔
1288
                }
1289

1290
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
228✔
1291
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
228✔
1292
                        return true;
×
1293
                }
1294
                return false;
228✔
1295
        }
1296

1297
        /**
1298
         * @param File $phpcsFile
1299
         * @param int  $stackPtr
1300
         *
1301
         * @return EnumInfo
1302
         */
1303
        public static function makeEnumInfo(File $phpcsFile, $stackPtr)
1304
        {
1305
                $tokens = $phpcsFile->getTokens();
4✔
1306
                $token = $tokens[$stackPtr];
4✔
1307

1308
                if (isset($token['scope_opener'])) {
4✔
1309
                        $blockStart = $token['scope_opener'];
2✔
1310
                        $blockEnd = $token['scope_closer'];
2✔
1311
                } else {
1✔
1312
                        // Enums before phpcs could detect them do not have scopes so we have to
1313
                        // find them ourselves.
1314

1315
                        $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
2✔
1316
                        if (! is_int($blockStart)) {
2✔
NEW
1317
                                throw new \Exception("Cannot find enum start at position {$stackPtr}");
×
1318
                        }
1319
                        $blockEnd = $tokens[$blockStart]['bracket_closer'];
2✔
1320
                }
1321

1322
                return new EnumInfo(
4✔
1323
                        $stackPtr,
4✔
1324
                        $blockStart,
4✔
1325
                        $blockEnd
2✔
1326
                );
4✔
1327
        }
1328

1329
        /**
1330
         * @param File $phpcsFile
1331
         * @param int  $stackPtr
1332
         *
1333
         * @return ForLoopInfo
1334
         */
1335
        public static function makeForLoopInfo(File $phpcsFile, $stackPtr)
1336
        {
1337
                $tokens = $phpcsFile->getTokens();
8✔
1338
                $token = $tokens[$stackPtr];
8✔
1339
                $forIndex = $stackPtr;
8✔
1340
                $blockStart = $token['parenthesis_closer'];
8✔
1341
                if (isset($token['scope_opener'])) {
8✔
1342
                        $blockStart = $token['scope_opener'];
8✔
1343
                        $blockEnd = $token['scope_closer'];
8✔
1344
                } else {
4✔
1345
                        // Some for loop blocks will not have scope positions because it they are
1346
                        // inline (no curly braces) so we have to find the end of their scope by
1347
                        // looking for the end of the next statement.
1348
                        $nextSemicolonIndex = $phpcsFile->findNext([T_SEMICOLON], $token['parenthesis_closer']);
8✔
1349
                        if (! is_int($nextSemicolonIndex)) {
8✔
1350
                                $nextSemicolonIndex = $token['parenthesis_closer'] + 1;
×
1351
                        }
1352
                        $blockEnd = $nextSemicolonIndex;
8✔
1353
                }
1354
                $initStart = intval($token['parenthesis_opener']) + 1;
8✔
1355
                $initEnd = null;
8✔
1356
                $conditionStart = null;
8✔
1357
                $conditionEnd = null;
8✔
1358
                $incrementStart = null;
8✔
1359
                $incrementEnd = $token['parenthesis_closer'] - 1;
8✔
1360

1361
                $semicolonCount = 0;
8✔
1362
                $forLoopLevel = $tokens[$forIndex]['level'];
8✔
1363
                $forLoopNestedParensCount = 1;
8✔
1364

1365
                if (isset($tokens[$forIndex]['nested_parenthesis'])) {
8✔
1366
                        $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
×
1367
                }
1368

1369
                for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
8✔
1370
                        if ($tokens[$i]['code'] !== T_SEMICOLON) {
8✔
1371
                                continue;
8✔
1372
                        }
1373

1374
                        if ($tokens[$i]['level'] !== $forLoopLevel) {
8✔
1375
                                continue;
8✔
1376
                        }
1377

1378
                        if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
8✔
1379
                                continue;
×
1380
                        }
1381

1382
                        switch ($semicolonCount) {
1383
                                case 0:
8✔
1384
                                        $initEnd = $i;
8✔
1385
                                        $conditionStart = $initEnd + 1;
8✔
1386
                                        break;
8✔
1387
                                case 1:
8✔
1388
                                        $conditionEnd = $i;
8✔
1389
                                        $incrementStart = $conditionEnd + 1;
8✔
1390
                                        break;
8✔
1391
                        }
1392
                        $semicolonCount += 1;
8✔
1393
                }
4✔
1394

1395
                if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
8✔
1396
                        throw new \Exception("Cannot parse for loop at position {$forIndex}");
×
1397
                }
1398

1399
                return new ForLoopInfo(
8✔
1400
                        $forIndex,
8✔
1401
                        $blockStart,
8✔
1402
                        $blockEnd,
8✔
1403
                        $initStart,
8✔
1404
                        $initEnd,
8✔
1405
                        $conditionStart,
8✔
1406
                        $conditionEnd,
8✔
1407
                        $incrementStart,
8✔
1408
                        $incrementEnd
4✔
1409
                );
8✔
1410
        }
1411

1412
        /**
1413
         * @param int                     $stackPtr
1414
         * @param array<int, ForLoopInfo> $forLoops
1415
         * @return ForLoopInfo|null
1416
         */
1417
        public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
1418
        {
1419
                foreach ($forLoops as $forLoop) {
340✔
1420
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1421
                                return $forLoop;
8✔
1422
                        }
1423
                }
170✔
1424
                return null;
340✔
1425
        }
1426

1427
        /**
1428
         * Return true if the token looks like constructor promotion.
1429
         *
1430
         * Call on a parameter variable token only.
1431
         *
1432
         * @param File $phpcsFile
1433
         * @param int  $stackPtr
1434
         *
1435
         * @return bool
1436
         */
1437
        public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
1438
        {
1439
                $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
220✔
1440
                if (! $functionIndex) {
220✔
1441
                        return false;
×
1442
                }
1443

1444
                $tokens = $phpcsFile->getTokens();
220✔
1445

1446
                // If the previous token is a visibility keyword, this is constructor
1447
                // promotion. eg: `public $foobar`.
1448
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), $functionIndex, true);
220✔
1449
                if (! is_int($prev)) {
220✔
1450
                        return false;
×
1451
                }
1452
                $prevToken = $tokens[$prev];
220✔
1453
                if (in_array($prevToken['code'], Tokens::$scopeModifiers, true)) {
220✔
1454
                        return true;
8✔
1455
                }
1456

1457
                // If the previous token is not a visibility keyword, but the one before it
1458
                // is, the previous token was probably a typehint and this is constructor
1459
                // promotion. eg: `public boolean $foobar`.
1460
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), $functionIndex, true);
220✔
1461
                if (! is_int($prev)) {
220✔
1462
                        return false;
×
1463
                }
1464
                $prevToken = $tokens[$prev];
220✔
1465
                if (in_array($prevToken['code'], Tokens::$scopeModifiers, true)) {
220✔
1466
                        return true;
8✔
1467
                }
1468

1469
                return false;
220✔
1470
        }
1471

1472
        /**
1473
         * Return true if the token is inside an abstract class.
1474
         *
1475
         * @param File $phpcsFile
1476
         * @param int  $stackPtr
1477
         *
1478
         * @return bool
1479
         */
1480
        public static function isInAbstractClass(File $phpcsFile, $stackPtr)
1481
        {
1482
                $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
108✔
1483
                if (! is_int($classIndex)) {
108✔
1484
                        return false;
92✔
1485
                }
1486
                $classProperties = $phpcsFile->getClassProperties($classIndex);
16✔
1487
                return $classProperties['is_abstract'];
16✔
1488
        }
1489

1490
        /**
1491
         * Return true if the function body is empty or contains only `return;`
1492
         *
1493
         * @param File $phpcsFile
1494
         * @param int  $stackPtr  The index of the function keyword.
1495
         *
1496
         * @return bool
1497
         */
1498
        public static function isFunctionBodyEmpty(File $phpcsFile, $stackPtr)
1499
        {
1500
                $tokens = $phpcsFile->getTokens();
8✔
1501
                if ($tokens[$stackPtr]['code'] !== T_FUNCTION) {
8✔
1502
                        return false;
×
1503
                }
1504
                $functionScopeStart = $tokens[$stackPtr]['scope_opener'];
8✔
1505
                $functionScopeEnd = $tokens[$stackPtr]['scope_closer'];
8✔
1506
                $tokensToIgnore = array_merge(
8✔
1507
                        Tokens::$emptyTokens,
8✔
1508
                        [
4✔
1509
                                T_RETURN,
8✔
1510
                                T_SEMICOLON,
8✔
1511
                                T_OPEN_CURLY_BRACKET,
8✔
1512
                                T_CLOSE_CURLY_BRACKET,
8✔
1513
                        ]
4✔
1514
                );
8✔
1515
                for ($i = $functionScopeStart; $i < $functionScopeEnd; $i++) {
8✔
1516
                        if (! in_array($tokens[$i]['code'], $tokensToIgnore, true)) {
8✔
1517
                                return false;
8✔
1518
                        }
1519
                }
4✔
1520
                return true;
8✔
1521
        }
1522
}
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