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

sirbrillig / phpcs-variable-analysis / 4576645380

pending completion
4576645380

push

github

GitHub
Fix scope closer detection in arrow function detection (#298)

40 of 53 new or added lines in 1 file covered. (75.47%)

1 existing line in 1 file now uncovered.

1544 of 1663 relevant lines covered (92.84%)

135.71 hits per line

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

87.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;
336✔
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;
340✔
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;
228✔
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;
336✔
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)) {
276✔
470
                        $functionPtr = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
224✔
471
                        if (! is_int($functionPtr)) {
224✔
472
                                throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
473
                        }
474
                        return $functionPtr;
224✔
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) {
152✔
511
                                $inClass = true;
60✔
512
                        }
30✔
513
                }
156✔
514

515
                if ($inClass) {
276✔
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;
60✔
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 ($tokens[$stackPtr]['content'] !== 'fn') {
24✔
632
                        return null;
×
633
                }
634
                // Make sure next non-space token is an open parenthesis
635
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
24✔
636
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
24✔
637
                        return null;
×
638
                }
639
                // Find the associated close parenthesis
640
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
24✔
641
                // Make sure the next token is a fat arrow or a return type
642
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
24✔
643
                if (! is_int($fatArrowIndex)) {
24✔
644
                        return null;
×
645
                }
646
                if (
647
                        $tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW &&
24✔
648
                        $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW' &&
24✔
649
                        $tokens[$fatArrowIndex]['code'] !== T_COLON
16✔
650
                ) {
12✔
UNCOV
651
                        return null;
×
652
                }
653

654
                // Find the scope closer
655
                $scopeCloserIndex = null;
24✔
656
                $foundCurlyPairs = 0;
24✔
657
                $foundArrayPairs = 0;
24✔
658
                $foundParenPairs = 0;
24✔
659
                $arrowBodyStart = $tokens[$stackPtr]['parenthesis_closer'] + 1;
24✔
660
                $lastToken = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
24✔
661
                for ($index = $arrowBodyStart; $index < $lastToken; $index++) {
24✔
662
                        $token = $tokens[$index];
24✔
663
                        if (empty($token['code'])) {
24✔
NEW
664
                                $scopeCloserIndex = $index;
×
NEW
665
                                break;
×
666
                        }
667

668
                        // A line break is always a closer.
669
                        if ($token['line'] !== $tokens[$stackPtr]['line']) {
24✔
NEW
670
                                $scopeCloserIndex = $index;
×
NEW
671
                                break;
×
672
                        }
673
                        $code = $token['code'];
24✔
674

675
                        // A semicolon is always a closer.
676
                        if ($code === T_SEMICOLON) {
24✔
677
                                $scopeCloserIndex = $index;
16✔
678
                                break;
16✔
679
                        }
680

681
                        // Track pair opening tokens.
682
                        if ($code === T_OPEN_CURLY_BRACKET) {
24✔
NEW
683
                                $foundCurlyPairs += 1;
×
NEW
684
                                continue;
×
685
                        }
686
                        if ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) {
24✔
687
                                $foundArrayPairs += 1;
16✔
688
                                continue;
16✔
689
                        }
690
                        if ($code === T_OPEN_PARENTHESIS) {
24✔
691
                                $foundParenPairs += 1;
16✔
692
                                continue;
16✔
693
                        }
694

695
                        // A pair closing is only an arrow func closer if there was no matching opening token.
696
                        if ($code === T_CLOSE_CURLY_BRACKET) {
24✔
NEW
697
                                if ($foundCurlyPairs === 0) {
×
NEW
698
                                        $scopeCloserIndex = $index;
×
NEW
699
                                        break;
×
700
                                }
NEW
701
                                $foundCurlyPairs -= 1;
×
NEW
702
                                continue;
×
703
                        }
704
                        if ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) {
24✔
705
                                if ($foundArrayPairs === 0) {
16✔
NEW
706
                                        $scopeCloserIndex = $index;
×
NEW
707
                                        break;
×
708
                                }
709
                                $foundArrayPairs -= 1;
16✔
710
                                continue;
16✔
711
                        }
712
                        if ($code === T_CLOSE_PARENTHESIS) {
24✔
713
                                if ($foundParenPairs === 0) {
16✔
714
                                        $scopeCloserIndex = $index;
8✔
715
                                        break;
8✔
716
                                }
717
                                $foundParenPairs -= 1;
16✔
718
                                continue;
16✔
719
                        }
720

721
                        // A comma is a closer only if we are not inside an opening token.
722
                        if ($code === T_COMMA) {
24✔
723
                                if (empty($foundArrayPairs) && empty($foundParenPairs) && empty($foundCurlyPairs)) {
16✔
724
                                        $scopeCloserIndex = $index;
16✔
725
                                        break;
16✔
726
                                }
727
                                continue;
8✔
728
                        }
729
                }
12✔
730

731
                if (! is_int($scopeCloserIndex)) {
24✔
732
                        return null;
×
733
                }
734

735
                return [
12✔
736
                        'scope_opener' => $stackPtr,
24✔
737
                        'scope_closer' => $scopeCloserIndex,
24✔
738
                ];
24✔
739
        }
740

741
        /**
742
         * Determine if a token is a list opener for list assignment/destructuring.
743
         *
744
         * The index provided can be either the opening square brace of a short list
745
         * assignment like the first character of `[$a] = $b;` or the `list` token of
746
         * an expression like `list($a) = $b;` or the opening parenthesis of that
747
         * expression.
748
         *
749
         * @param File $phpcsFile
750
         * @param int  $listOpenerIndex
751
         *
752
         * @return bool
753
         */
754
        private static function isListAssignment(File $phpcsFile, $listOpenerIndex)
755
        {
756
                $tokens = $phpcsFile->getTokens();
44✔
757
                // Match `[$a] = $b;` except for when the previous token is a parenthesis.
758
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SHORT_ARRAY) {
44✔
759
                        return true;
28✔
760
                }
761
                // Match `list($a) = $b;`
762
                if ($tokens[$listOpenerIndex]['code'] === T_LIST) {
44✔
763
                        return true;
32✔
764
                }
765

766
                // If $listOpenerIndex is the open parenthesis of `list($a) = $b;`, then
767
                // match that too.
768
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_PARENTHESIS) {
32✔
769
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
4✔
770
                        if (
771
                                isset($tokens[$previousTokenPtr])
4✔
772
                                && $tokens[$previousTokenPtr]['code'] === T_LIST
4✔
773
                        ) {
2✔
774
                                return true;
4✔
775
                        }
776
                        return true;
×
777
                }
778

779
                // If the list opener token is a square bracket that is preceeded by a
780
                // close parenthesis that has an owner which is a scope opener, then this
781
                // is a list assignment and not an array access.
782
                //
783
                // Match `if (true) [$a] = $b;`
784
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SQUARE_BRACKET) {
28✔
785
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
28✔
786
                        if (
787
                                isset($tokens[$previousTokenPtr])
28✔
788
                                && $tokens[$previousTokenPtr]['code'] === T_CLOSE_PARENTHESIS
28✔
789
                                && isset($tokens[$previousTokenPtr]['parenthesis_owner'])
28✔
790
                                && isset(Tokens::$scopeOpeners[$tokens[$tokens[$previousTokenPtr]['parenthesis_owner']]['code']])
28✔
791
                        ) {
14✔
792
                                return true;
4✔
793
                        }
794
                }
14✔
795

796
                return false;
28✔
797
        }
798

799
        /**
800
         * Return a list of indices for variables assigned within a list assignment.
801
         *
802
         * The index provided can be either the opening square brace of a short list
803
         * assignment like the first character of `[$a] = $b;` or the `list` token of
804
         * an expression like `list($a) = $b;` or the opening parenthesis of that
805
         * expression.
806
         *
807
         * @param File $phpcsFile
808
         * @param int  $listOpenerIndex
809
         *
810
         * @return ?array<int>
811
         */
812
        public static function getListAssignments(File $phpcsFile, $listOpenerIndex)
813
        {
814
                $tokens = $phpcsFile->getTokens();
60✔
815
                self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]);
60✔
816

817
                // First find the end of the list
818
                $closePtr = null;
60✔
819
                if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) {
60✔
820
                        $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer'];
40✔
821
                }
20✔
822
                if (isset($tokens[$listOpenerIndex]['bracket_closer'])) {
60✔
823
                        $closePtr = $tokens[$listOpenerIndex]['bracket_closer'];
60✔
824
                }
30✔
825
                if (! $closePtr) {
60✔
826
                        return null;
×
827
                }
828

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

832
                // If the next token isn't an assignment, check for nested brackets because we might be a nested assignment
833
                if (! is_int($assignPtr) || $tokens[$assignPtr]['code'] !== T_EQUAL) {
60✔
834
                        // Collect the enclosing list open/close tokens ($parents is an assoc array keyed by opener index and the value is the closer index)
835
                        $parents = isset($tokens[$listOpenerIndex]['nested_parenthesis']) ? $tokens[$listOpenerIndex]['nested_parenthesis'] : [];
40✔
836
                        // There's no record of nested brackets for short lists; we'll have to find the parent ourselves
837
                        if (empty($parents)) {
40✔
838
                                $parentSquareBracket = self::findContainingOpeningSquareBracket($phpcsFile, $listOpenerIndex);
40✔
839
                                if (is_int($parentSquareBracket)) {
40✔
840
                                        // Collect the opening index, but we don't actually need the closing paren index so just make that 0
841
                                        $parents = [$parentSquareBracket => 0];
4✔
842
                                }
2✔
843
                        }
20✔
844
                        // If we have no parents, this is not a nested assignment and therefore is not an assignment
845
                        if (empty($parents)) {
40✔
846
                                return null;
36✔
847
                        }
848

849
                        // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion)
850
                        $isNestedAssignment = null;
20✔
851
                        $parentListOpener = array_keys(array_reverse($parents, true))[0];
20✔
852
                        $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener);
20✔
853
                        if ($isNestedAssignment === null) {
20✔
854
                                return null;
16✔
855
                        }
856
                }
2✔
857

858
                $variablePtrs = [];
44✔
859

860
                $currentPtr = $listOpenerIndex;
44✔
861
                $variablePtr = 0;
44✔
862
                while ($currentPtr < $closePtr && is_int($variablePtr)) {
44✔
863
                        $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr);
44✔
864
                        if (is_int($variablePtr)) {
44✔
865
                                $variablePtrs[] = $variablePtr;
32✔
866
                        }
16✔
867
                        ++$currentPtr;
44✔
868
                }
22✔
869

870
                if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) {
44✔
871
                        return null;
28✔
872
                }
873

874
                return $variablePtrs;
32✔
875
        }
876

877
        /**
878
         * @param File $phpcsFile
879
         * @param int  $stackPtr
880
         *
881
         * @return string[]
882
         */
883
        public static function getVariablesDefinedByArrowFunction(File $phpcsFile, $stackPtr)
884
        {
885
                $tokens = $phpcsFile->getTokens();
24✔
886
                $arrowFunctionToken = $tokens[$stackPtr];
24✔
887
                $variableNames = [];
24✔
888
                self::debug('looking for variables in arrow function token', $arrowFunctionToken);
24✔
889
                for ($index = $arrowFunctionToken['parenthesis_opener']; $index < $arrowFunctionToken['parenthesis_closer']; $index++) {
24✔
890
                        $token = $tokens[$index];
24✔
891
                        if ($token['code'] === T_VARIABLE) {
24✔
892
                                $variableNames[] = self::normalizeVarName($token['content']);
24✔
893
                        }
12✔
894
                }
12✔
895
                self::debug('found these variables in arrow function token', $variableNames);
24✔
896
                return $variableNames;
24✔
897
        }
898

899
        /**
900
         * @return void
901
         */
902
        public static function debug()
903
        {
904
                $messages = func_get_args();
344✔
905
                if (! defined('PHP_CODESNIFFER_VERBOSITY')) {
344✔
906
                        return;
×
907
                }
908
                if (PHP_CODESNIFFER_VERBOSITY <= 3) {
344✔
909
                        return;
344✔
910
                }
911
                $output = PHP_EOL . 'VariableAnalysisSniff: DEBUG:';
×
912
                foreach ($messages as $message) {
×
913
                        if (is_string($message) || is_numeric($message)) {
×
914
                                $output .= ' "' . $message . '"';
×
915
                                continue;
×
916
                        }
917
                        $output .= PHP_EOL . var_export($message, true) . PHP_EOL;
×
918
                }
919
                $output .= PHP_EOL;
×
920
                echo $output;
×
921
        }
922

923
        /**
924
         * @param string $pattern
925
         * @param string $value
926
         *
927
         * @return string[]
928
         */
929
        public static function splitStringToArray($pattern, $value)
930
        {
931
                $result = preg_split($pattern, $value);
24✔
932
                return is_array($result) ? $result : [];
24✔
933
        }
934

935
        /**
936
         * @param string $varName
937
         *
938
         * @return bool
939
         */
940
        public static function isVariableANumericVariable($varName)
941
        {
942
                return is_numeric(substr($varName, 0, 1));
328✔
943
        }
944

945
        /**
946
         * @param File $phpcsFile
947
         * @param int  $stackPtr
948
         *
949
         * @return bool
950
         */
951
        public static function isVariableInsideElseCondition(File $phpcsFile, $stackPtr)
952
        {
953
                $tokens = $phpcsFile->getTokens();
320✔
954
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
320✔
955
                $nonFunctionTokenTypes[] = T_OPEN_PARENTHESIS;
320✔
956
                $nonFunctionTokenTypes[] = T_INLINE_HTML;
320✔
957
                $nonFunctionTokenTypes[] = T_CLOSE_TAG;
320✔
958
                $nonFunctionTokenTypes[] = T_VARIABLE;
320✔
959
                $nonFunctionTokenTypes[] = T_ELLIPSIS;
320✔
960
                $nonFunctionTokenTypes[] = T_COMMA;
320✔
961
                $nonFunctionTokenTypes[] = T_STRING;
320✔
962
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
320✔
963
                $elsePtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $stackPtr - 1, null, true, null, true));
320✔
964
                $elseTokenTypes = [
160✔
965
                        T_ELSE,
320✔
966
                        T_ELSEIF,
320✔
967
                ];
320✔
968
                if (is_int($elsePtr) && in_array($tokens[$elsePtr]['code'], $elseTokenTypes, true)) {
320✔
969
                        return true;
16✔
970
                }
971
                return false;
320✔
972
        }
973

974
        /**
975
         * @param File $phpcsFile
976
         * @param int  $stackPtr
977
         *
978
         * @return bool
979
         */
980
        public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr)
981
        {
982
                $tokens = $phpcsFile->getTokens();
320✔
983
                $token = $tokens[$stackPtr];
320✔
984
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
320✔
985
                $elseTokenTypes = [
160✔
986
                        T_ELSE,
320✔
987
                        T_ELSEIF,
320✔
988
                ];
320✔
989
                foreach (array_reverse($conditions, true) as $scopeCode) {
320✔
990
                        if (in_array($scopeCode, $elseTokenTypes, true)) {
308✔
991
                                return true;
8✔
992
                        }
993
                }
160✔
994

995
                // Some else body code will not have conditions because it is inline (no
996
                // curly braces) so we have to look in other ways.
997
                $previousSemicolonPtr = $phpcsFile->findPrevious([T_SEMICOLON], $stackPtr - 1);
320✔
998
                if (! is_int($previousSemicolonPtr)) {
320✔
999
                        $previousSemicolonPtr = 0;
148✔
1000
                }
74✔
1001
                $elsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1, $previousSemicolonPtr);
320✔
1002
                if (is_int($elsePtr)) {
320✔
1003
                        return true;
8✔
1004
                }
1005

1006
                return false;
320✔
1007
        }
1008

1009
        /**
1010
         * @param File $phpcsFile
1011
         * @param int  $stackPtr
1012
         *
1013
         * @return int[]
1014
         */
1015
        public static function getAttachedBlockIndicesForElse(File $phpcsFile, $stackPtr)
1016
        {
1017
                $currentElsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1);
16✔
1018
                if (! is_int($currentElsePtr)) {
16✔
1019
                        throw new \Exception("Cannot find expected else at {$stackPtr}");
×
1020
                }
1021

1022
                $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
16✔
1023
                if (! is_int($ifPtr)) {
16✔
1024
                        throw new \Exception("Cannot find if for else at {$stackPtr}");
×
1025
                }
1026
                $blockIndices = [$ifPtr];
16✔
1027

1028
                $previousElseIfPtr = $currentElsePtr;
16✔
1029
                do {
1030
                        $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
16✔
1031
                        if (is_int($elseIfPtr)) {
16✔
1032
                                $blockIndices[] = $elseIfPtr;
16✔
1033
                                $previousElseIfPtr = $elseIfPtr;
16✔
1034
                        }
8✔
1035
                } while (is_int($elseIfPtr));
16✔
1036

1037
                return $blockIndices;
16✔
1038
        }
1039

1040
        /**
1041
         * @param int $needle
1042
         * @param int $scopeStart
1043
         * @param int $scopeEnd
1044
         *
1045
         * @return bool
1046
         */
1047
        public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
1048
        {
1049
                return ($needle > $scopeStart && $needle < $scopeEnd);
16✔
1050
        }
1051

1052
        /**
1053
         * @param File $phpcsFile
1054
         * @param int  $scopeStartIndex
1055
         *
1056
         * @return int
1057
         */
1058
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
1059
        {
1060
                $tokens = $phpcsFile->getTokens();
344✔
1061
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
344✔
1062

1063
                if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) {
344✔
1064
                        $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex);
24✔
1065
                        $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex;
24✔
1066
                }
12✔
1067

1068
                if ($scopeStartIndex === 0) {
344✔
1069
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
344✔
1070
                }
172✔
1071
                return $scopeCloserIndex;
344✔
1072
        }
1073

1074
        /**
1075
         * @param File $phpcsFile
1076
         *
1077
         * @return int
1078
         */
1079
        public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
1080
        {
1081
                $tokens = $phpcsFile->getTokens();
344✔
1082
                foreach (array_reverse($tokens, true) as $index => $token) {
344✔
1083
                        if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
344✔
1084
                                return $index;
344✔
1085
                        }
1086
                }
168✔
1087
                self::debug('no non-empty token found for end of file');
×
1088
                return 0;
×
1089
        }
1090

1091
        /**
1092
         * @param VariableInfo $varInfo
1093
         * @param ScopeInfo    $scopeInfo
1094
         *
1095
         * @return bool
1096
         */
1097
        public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo)
1098
        {
1099
                $foundVarPosition = false;
64✔
1100
                foreach ($scopeInfo->variables as $variable) {
64✔
1101
                        if ($variable === $varInfo) {
64✔
1102
                                $foundVarPosition = true;
64✔
1103
                                continue;
64✔
1104
                        }
1105
                        if (! $foundVarPosition) {
44✔
1106
                                continue;
36✔
1107
                        }
1108
                        if ($variable->scopeType !== ScopeType::PARAM) {
44✔
1109
                                continue;
36✔
1110
                        }
1111
                        if ($variable->firstRead) {
16✔
1112
                                return true;
16✔
1113
                        }
1114
                }
32✔
1115
                return false;
64✔
1116
        }
1117

1118
        /**
1119
         * @param File         $phpcsFile
1120
         * @param VariableInfo $varInfo
1121
         * @param ScopeInfo    $scopeInfo
1122
         *
1123
         * @return bool
1124
         */
1125
        public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
1126
        {
1127
                $requireTokens = [
2✔
1128
                        T_REQUIRE,
4✔
1129
                        T_REQUIRE_ONCE,
4✔
1130
                        T_INCLUDE,
4✔
1131
                        T_INCLUDE_ONCE,
4✔
1132
                ];
4✔
1133
                $indexToStartSearch = $varInfo->firstDeclared;
4✔
1134
                if (! empty($varInfo->firstInitialized)) {
4✔
1135
                        $indexToStartSearch = $varInfo->firstInitialized;
4✔
1136
                }
2✔
1137
                $tokens = $phpcsFile->getTokens();
4✔
1138
                $indexToStopSearch = isset($tokens[$scopeInfo->scopeStartIndex]['scope_closer']) ? $tokens[$scopeInfo->scopeStartIndex]['scope_closer'] : null;
4✔
1139
                if (! is_int($indexToStartSearch) || ! is_int($indexToStopSearch)) {
4✔
1140
                        return false;
×
1141
                }
1142
                $requireTokenIndex = $phpcsFile->findNext($requireTokens, $indexToStartSearch + 1, $indexToStopSearch);
4✔
1143
                if (is_int($requireTokenIndex)) {
4✔
1144
                        return true;
4✔
1145
                }
1146
                return false;
×
1147
        }
1148

1149
        /**
1150
         * Find the index of the function keyword for a token in a function call's arguments
1151
         *
1152
         * For the variable `$foo` in the expression `doSomething($foo)`, this will
1153
         * return the index of the `doSomething` token.
1154
         *
1155
         * @param File $phpcsFile
1156
         * @param int  $stackPtr
1157
         *
1158
         * @return ?int
1159
         */
1160
        public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr)
1161
        {
1162
                $tokens = $phpcsFile->getTokens();
332✔
1163
                $token = $tokens[$stackPtr];
332✔
1164
                if (empty($token['nested_parenthesis'])) {
332✔
1165
                        return null;
324✔
1166
                }
1167
                /**
1168
                 * @var array<int|string|null>
1169
                 */
1170
                $startingParenthesis = array_keys($token['nested_parenthesis']);
208✔
1171
                $startOfArguments = end($startingParenthesis);
208✔
1172
                if (! is_int($startOfArguments)) {
208✔
1173
                        return null;
×
1174
                }
1175

1176
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
208✔
1177
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
208✔
1178
                if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
208✔
1179
                        return null;
×
1180
                }
1181
                if (
1182
                        $tokens[$functionPtr]['content'] === 'function'
208✔
1183
                        || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))
208✔
1184
                ) {
104✔
1185
                        // If there is a function/fn keyword before the beginning of the parens,
1186
                        // this is a function definition and not a function call.
1187
                        return null;
×
1188
                }
1189
                if (! empty($tokens[$functionPtr]['scope_opener'])) {
208✔
1190
                        // If the alleged function name has a scope, this is not a function call.
1191
                        return null;
130✔
1192
                }
1193

1194
                $functionNameType = $tokens[$functionPtr]['code'];
166✔
1195
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
166✔
1196
                        // If the alleged function name is not a variable or a string, this is
1197
                        // not a function call.
1198
                        return null;
48✔
1199
                }
1200

1201
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
162✔
1202
                        // If the variable is inside a different scope than the function name,
1203
                        // the function call doesn't apply to the variable.
1204
                        return null;
28✔
1205
                }
1206

1207
                return $functionPtr;
162✔
1208
        }
1209

1210
        /**
1211
         * @param File $phpcsFile
1212
         * @param int  $stackPtr
1213
         *
1214
         * @return bool
1215
         */
1216
        public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
1217
        {
1218
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
320✔
1219
                if (! is_int($functionIndex)) {
320✔
1220
                        return false;
302✔
1221
                }
1222
                $tokens = $phpcsFile->getTokens();
162✔
1223
                if (! isset($tokens[$functionIndex])) {
162✔
1224
                        return false;
×
1225
                }
1226
                $allowedFunctionNames = [
81✔
1227
                        'isset',
162✔
1228
                        'empty',
162✔
1229
                ];
162✔
1230
                if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) {
162✔
1231
                        return true;
4✔
1232
                }
1233
                return false;
162✔
1234
        }
1235

1236
        /**
1237
         * @param File $phpcsFile
1238
         * @param int  $stackPtr
1239
         *
1240
         * @return bool
1241
         */
1242
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
1243
        {
1244
                $tokens = $phpcsFile->getTokens();
248✔
1245
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
248✔
1246

1247
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
248✔
1248
                if (! is_int($arrayPushOperatorIndex1)) {
248✔
1249
                        return false;
×
1250
                }
1251
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
248✔
1252
                        return false;
248✔
1253
                }
1254

1255
                $arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true));
36✔
1256
                if (! is_int($arrayPushOperatorIndex2)) {
36✔
1257
                        return false;
×
1258
                }
1259
                if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') {
36✔
1260
                        return false;
8✔
1261
                }
1262

1263
                $arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true));
28✔
1264
                if (! is_int($arrayPushOperatorIndex3)) {
28✔
1265
                        return false;
×
1266
                }
1267
                if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') {
28✔
1268
                        return false;
×
1269
                }
1270

1271
                return true;
28✔
1272
        }
1273

1274
        /**
1275
         * @param File $phpcsFile
1276
         * @param int  $stackPtr
1277
         *
1278
         * @return bool
1279
         */
1280
        public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
1281
        {
1282
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
248✔
1283
                if (! is_int($functionIndex)) {
248✔
1284
                        return false;
218✔
1285
                }
1286
                $tokens = $phpcsFile->getTokens();
98✔
1287
                if (! isset($tokens[$functionIndex])) {
98✔
1288
                        return false;
×
1289
                }
1290
                if ($tokens[$functionIndex]['content'] === 'unset') {
98✔
1291
                        return true;
8✔
1292
                }
1293
                return false;
90✔
1294
        }
1295

1296
        /**
1297
         * @param File $phpcsFile
1298
         * @param int  $stackPtr
1299
         *
1300
         * @return bool
1301
         */
1302
        public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr)
1303
        {
1304
                $previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
312✔
1305
                if (! is_int($previousStatementPtr)) {
312✔
1306
                        $previousStatementPtr = 1;
32✔
1307
                }
16✔
1308
                $previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
312✔
1309
                if (is_int($previousTokenPtr)) {
312✔
1310
                        return true;
4✔
1311
                }
1312
                return false;
312✔
1313
        }
1314

1315
        /**
1316
         * @param File $phpcsFile
1317
         * @param int  $stackPtr
1318
         *
1319
         * @return bool
1320
         */
1321
        public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
1322
        {
1323
                // Is the next non-whitespace an assignment?
1324
                $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
332✔
1325
                if (! is_int($assignPtr)) {
332✔
1326
                        return false;
324✔
1327
                }
1328

1329
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1330
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
312✔
1331
                        self::debug('found variable variable');
4✔
1332
                        return false;
4✔
1333
                }
1334
                return true;
312✔
1335
        }
1336

1337
        /**
1338
         * @param File $phpcsFile
1339
         * @param int  $stackPtr
1340
         *
1341
         * @return bool
1342
         */
1343
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
1344
        {
1345
                $tokens = $phpcsFile->getTokens();
312✔
1346

1347
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
312✔
1348
                if ($prev === false) {
312✔
1349
                        return false;
×
1350
                }
1351
                if ($tokens[$prev]['code'] === T_DOLLAR) {
312✔
1352
                        return true;
4✔
1353
                }
1354
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
312✔
1355
                        return false;
256✔
1356
                }
1357

1358
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
228✔
1359
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
228✔
1360
                        return true;
×
1361
                }
1362
                return false;
228✔
1363
        }
1364

1365
        /**
1366
         * @param File $phpcsFile
1367
         * @param int  $stackPtr
1368
         *
1369
         * @return EnumInfo|null
1370
         */
1371
        public static function makeEnumInfo(File $phpcsFile, $stackPtr)
1372
        {
1373
                $tokens = $phpcsFile->getTokens();
4✔
1374
                $token = $tokens[$stackPtr];
4✔
1375

1376
                if (isset($token['scope_opener'])) {
4✔
1377
                        $blockStart = $token['scope_opener'];
2✔
1378
                        $blockEnd = $token['scope_closer'];
2✔
1379
                } else {
1✔
1380
                        // Enums before phpcs could detect them do not have scopes so we have to
1381
                        // find them ourselves.
1382

1383
                        $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
4✔
1384
                        if (! is_int($blockStart)) {
4✔
1385
                                return null;
4✔
1386
                        }
1387
                        $blockEnd = $tokens[$blockStart]['bracket_closer'];
2✔
1388
                }
1389

1390
                return new EnumInfo(
4✔
1391
                        $stackPtr,
4✔
1392
                        $blockStart,
4✔
1393
                        $blockEnd
2✔
1394
                );
4✔
1395
        }
1396

1397
        /**
1398
         * @param File $phpcsFile
1399
         * @param int  $stackPtr
1400
         *
1401
         * @return ForLoopInfo
1402
         */
1403
        public static function makeForLoopInfo(File $phpcsFile, $stackPtr)
1404
        {
1405
                $tokens = $phpcsFile->getTokens();
8✔
1406
                $token = $tokens[$stackPtr];
8✔
1407
                $forIndex = $stackPtr;
8✔
1408
                $blockStart = $token['parenthesis_closer'];
8✔
1409
                if (isset($token['scope_opener'])) {
8✔
1410
                        $blockStart = $token['scope_opener'];
8✔
1411
                        $blockEnd = $token['scope_closer'];
8✔
1412
                } else {
4✔
1413
                        // Some for loop blocks will not have scope positions because it they are
1414
                        // inline (no curly braces) so we have to find the end of their scope by
1415
                        // looking for the end of the next statement.
1416
                        $nextSemicolonIndex = $phpcsFile->findNext([T_SEMICOLON], $token['parenthesis_closer']);
8✔
1417
                        if (! is_int($nextSemicolonIndex)) {
8✔
1418
                                $nextSemicolonIndex = $token['parenthesis_closer'] + 1;
×
1419
                        }
1420
                        $blockEnd = $nextSemicolonIndex;
8✔
1421
                }
1422
                $initStart = intval($token['parenthesis_opener']) + 1;
8✔
1423
                $initEnd = null;
8✔
1424
                $conditionStart = null;
8✔
1425
                $conditionEnd = null;
8✔
1426
                $incrementStart = null;
8✔
1427
                $incrementEnd = $token['parenthesis_closer'] - 1;
8✔
1428

1429
                $semicolonCount = 0;
8✔
1430
                $forLoopLevel = $tokens[$forIndex]['level'];
8✔
1431
                $forLoopNestedParensCount = 1;
8✔
1432

1433
                if (isset($tokens[$forIndex]['nested_parenthesis'])) {
8✔
1434
                        $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
×
1435
                }
1436

1437
                for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
8✔
1438
                        if ($tokens[$i]['code'] !== T_SEMICOLON) {
8✔
1439
                                continue;
8✔
1440
                        }
1441

1442
                        if ($tokens[$i]['level'] !== $forLoopLevel) {
8✔
1443
                                continue;
8✔
1444
                        }
1445

1446
                        if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
8✔
1447
                                continue;
×
1448
                        }
1449

1450
                        switch ($semicolonCount) {
1451
                                case 0:
8✔
1452
                                        $initEnd = $i;
8✔
1453
                                        $conditionStart = $initEnd + 1;
8✔
1454
                                        break;
8✔
1455
                                case 1:
8✔
1456
                                        $conditionEnd = $i;
8✔
1457
                                        $incrementStart = $conditionEnd + 1;
8✔
1458
                                        break;
8✔
1459
                        }
1460
                        $semicolonCount += 1;
8✔
1461
                }
4✔
1462

1463
                if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
8✔
1464
                        throw new \Exception("Cannot parse for loop at position {$forIndex}");
×
1465
                }
1466

1467
                return new ForLoopInfo(
8✔
1468
                        $forIndex,
8✔
1469
                        $blockStart,
8✔
1470
                        $blockEnd,
8✔
1471
                        $initStart,
8✔
1472
                        $initEnd,
8✔
1473
                        $conditionStart,
8✔
1474
                        $conditionEnd,
8✔
1475
                        $incrementStart,
8✔
1476
                        $incrementEnd
4✔
1477
                );
8✔
1478
        }
1479

1480
        /**
1481
         * @param int                     $stackPtr
1482
         * @param array<int, ForLoopInfo> $forLoops
1483
         * @return ForLoopInfo|null
1484
         */
1485
        public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
1486
        {
1487
                foreach ($forLoops as $forLoop) {
340✔
1488
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1489
                                return $forLoop;
8✔
1490
                        }
1491
                }
170✔
1492
                return null;
340✔
1493
        }
1494

1495
        /**
1496
         * Return true if the token looks like constructor promotion.
1497
         *
1498
         * Call on a parameter variable token only.
1499
         *
1500
         * @param File $phpcsFile
1501
         * @param int  $stackPtr
1502
         *
1503
         * @return bool
1504
         */
1505
        public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
1506
        {
1507
                $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
224✔
1508
                if (! $functionIndex) {
224✔
1509
                        return false;
×
1510
                }
1511

1512
                $tokens = $phpcsFile->getTokens();
224✔
1513

1514
                // If the previous token is a visibility keyword, this is constructor
1515
                // promotion. eg: `public $foobar`.
1516
                $prevIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), $functionIndex, true);
224✔
1517
                if (! is_int($prevIndex)) {
224✔
1518
                        return false;
×
1519
                }
1520
                $prevToken = $tokens[$prevIndex];
224✔
1521
                if (in_array($prevToken['code'], Tokens::$scopeModifiers, true)) {
224✔
1522
                        return true;
8✔
1523
                }
1524

1525
                // If the previous token is not a visibility keyword, but the one before it
1526
                // is, the previous token was probably a typehint and this is constructor
1527
                // promotion. eg: `public boolean $foobar`.
1528
                $prev2Index = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevIndex - 1), $functionIndex, true);
224✔
1529
                if (! is_int($prev2Index)) {
224✔
1530
                        return false;
×
1531
                }
1532
                $prev2Token = $tokens[$prev2Index];
224✔
1533
                if (in_array($prev2Token['code'], Tokens::$scopeModifiers, true)) {
224✔
1534
                        return true;
12✔
1535
                }
1536

1537
                // If the previous token is not a visibility keyword, but the one two
1538
                // before it is, and one of the tokens is `readonly`, the previous token
1539
                // was probably a typehint and this is constructor promotion. eg: `public
1540
                // readonly boolean $foobar`.
1541
                $prev3Index = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev2Index - 1), $functionIndex, true);
220✔
1542
                if (! is_int($prev3Index)) {
220✔
1543
                        return false;
44✔
1544
                }
1545
                $prev3Token = $tokens[$prev3Index];
220✔
1546
                $wasPreviousReadonly = $prevToken['content'] === 'readonly' || $prev2Token['content'] === 'readonly';
220✔
1547
                if (in_array($prev3Token['code'], Tokens::$scopeModifiers, true) && $wasPreviousReadonly) {
220✔
1548
                        return true;
8✔
1549
                }
1550

1551
                return false;
220✔
1552
        }
1553

1554
        /**
1555
         * Return true if the token is inside an abstract class.
1556
         *
1557
         * @param File $phpcsFile
1558
         * @param int  $stackPtr
1559
         *
1560
         * @return bool
1561
         */
1562
        public static function isInAbstractClass(File $phpcsFile, $stackPtr)
1563
        {
1564
                $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
108✔
1565
                if (! is_int($classIndex)) {
108✔
1566
                        return false;
92✔
1567
                }
1568
                $classProperties = $phpcsFile->getClassProperties($classIndex);
16✔
1569
                return $classProperties['is_abstract'];
16✔
1570
        }
1571

1572
        /**
1573
         * Return true if the function body is empty or contains only `return;`
1574
         *
1575
         * @param File $phpcsFile
1576
         * @param int  $stackPtr  The index of the function keyword.
1577
         *
1578
         * @return bool
1579
         */
1580
        public static function isFunctionBodyEmpty(File $phpcsFile, $stackPtr)
1581
        {
1582
                $tokens = $phpcsFile->getTokens();
8✔
1583
                if ($tokens[$stackPtr]['code'] !== T_FUNCTION) {
8✔
1584
                        return false;
×
1585
                }
1586
                $functionScopeStart = $tokens[$stackPtr]['scope_opener'];
8✔
1587
                $functionScopeEnd = $tokens[$stackPtr]['scope_closer'];
8✔
1588
                $tokensToIgnore = array_merge(
8✔
1589
                        Tokens::$emptyTokens,
8✔
1590
                        [
4✔
1591
                                T_RETURN,
8✔
1592
                                T_SEMICOLON,
8✔
1593
                                T_OPEN_CURLY_BRACKET,
8✔
1594
                                T_CLOSE_CURLY_BRACKET,
8✔
1595
                        ]
4✔
1596
                );
8✔
1597
                for ($i = $functionScopeStart; $i < $functionScopeEnd; $i++) {
8✔
1598
                        if (! in_array($tokens[$i]['code'], $tokensToIgnore, true)) {
8✔
1599
                                return false;
8✔
1600
                        }
1601
                }
4✔
1602
                return true;
8✔
1603
        }
1604
}
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