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

sirbrillig / phpcs-variable-analysis / 4569827015

pending completion
4569827015

Pull #296

github

GitHub
Merge 07ffaca08 into 11b943301
Pull Request #296: Use custom logic for finding arrow function scope

1512 of 1618 relevant lines covered (93.45%)

139.11 hits per line

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

88.49
/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
642
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
24✔
643
                if (! is_int($fatArrowIndex)) {
24✔
644
                        return null;
×
645
                }
646
                if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') {
24✔
647
                        return null;
×
648
                }
649
                // Find the scope closer
650
                $endScopeTokens = [
12✔
651
                        T_COMMA,
24✔
652
                        T_SEMICOLON,
24✔
653
                        T_CLOSE_PARENTHESIS,
24✔
654
                        T_CLOSE_CURLY_BRACKET,
24✔
655
                        T_CLOSE_SHORT_ARRAY,
24✔
656
                ];
24✔
657
                $scopeCloserIndex = $phpcsFile->findNext($endScopeTokens, $fatArrowIndex        + 1);
24✔
658
                if (! is_int($scopeCloserIndex)) {
24✔
659
                        return null;
×
660
                }
661
                return [
12✔
662
                        'scope_opener' => $stackPtr,
24✔
663
                        'scope_closer' => $scopeCloserIndex,
24✔
664
                ];
24✔
665
        }
666

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

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

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

722
                return false;
28✔
723
        }
724

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

743
                // First find the end of the list
744
                $closePtr = null;
60✔
745
                if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) {
60✔
746
                        $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer'];
40✔
747
                }
20✔
748
                if (isset($tokens[$listOpenerIndex]['bracket_closer'])) {
60✔
749
                        $closePtr = $tokens[$listOpenerIndex]['bracket_closer'];
60✔
750
                }
30✔
751
                if (! $closePtr) {
60✔
752
                        return null;
×
753
                }
754

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

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

775
                        // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion)
776
                        $isNestedAssignment = null;
20✔
777
                        $parentListOpener = array_keys(array_reverse($parents, true))[0];
20✔
778
                        $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener);
20✔
779
                        if ($isNestedAssignment === null) {
20✔
780
                                return null;
16✔
781
                        }
782
                }
2✔
783

784
                $variablePtrs = [];
44✔
785

786
                $currentPtr = $listOpenerIndex;
44✔
787
                $variablePtr = 0;
44✔
788
                while ($currentPtr < $closePtr && is_int($variablePtr)) {
44✔
789
                        $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr);
44✔
790
                        if (is_int($variablePtr)) {
44✔
791
                                $variablePtrs[] = $variablePtr;
32✔
792
                        }
16✔
793
                        ++$currentPtr;
44✔
794
                }
22✔
795

796
                if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) {
44✔
797
                        return null;
28✔
798
                }
799

800
                return $variablePtrs;
32✔
801
        }
802

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

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

849
        /**
850
         * @param string $pattern
851
         * @param string $value
852
         *
853
         * @return string[]
854
         */
855
        public static function splitStringToArray($pattern, $value)
856
        {
857
                $result = preg_split($pattern, $value);
24✔
858
                return is_array($result) ? $result : [];
24✔
859
        }
860

861
        /**
862
         * @param string $varName
863
         *
864
         * @return bool
865
         */
866
        public static function isVariableANumericVariable($varName)
867
        {
868
                return is_numeric(substr($varName, 0, 1));
328✔
869
        }
870

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

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

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

932
                return false;
320✔
933
        }
934

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

948
                $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
16✔
949
                if (! is_int($ifPtr)) {
16✔
950
                        throw new \Exception("Cannot find if for else at {$stackPtr}");
×
951
                }
952
                $blockIndices = [$ifPtr];
16✔
953

954
                $previousElseIfPtr = $currentElsePtr;
16✔
955
                do {
956
                        $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
16✔
957
                        if (is_int($elseIfPtr)) {
16✔
958
                                $blockIndices[] = $elseIfPtr;
16✔
959
                                $previousElseIfPtr = $elseIfPtr;
16✔
960
                        }
8✔
961
                } while (is_int($elseIfPtr));
16✔
962

963
                return $blockIndices;
16✔
964
        }
965

966
        /**
967
         * @param int $needle
968
         * @param int $scopeStart
969
         * @param int $scopeEnd
970
         *
971
         * @return bool
972
         */
973
        public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
974
        {
975
                return ($needle > $scopeStart && $needle < $scopeEnd);
16✔
976
        }
977

978
        /**
979
         * @param File $phpcsFile
980
         * @param int  $scopeStartIndex
981
         *
982
         * @return int
983
         */
984
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
985
        {
986
                $tokens = $phpcsFile->getTokens();
344✔
987
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
344✔
988

989
                if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) {
344✔
990
                        $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex);
24✔
991
                        $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex;
24✔
992
                }
12✔
993

994
                if ($scopeStartIndex === 0) {
344✔
995
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
344✔
996
                }
172✔
997
                return $scopeCloserIndex;
344✔
998
        }
999

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

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

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

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

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

1120
                $functionNameType = $tokens[$functionPtr]['code'];
166✔
1121
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
166✔
1122
                        // If the alleged function name is not a variable or a string, this is
1123
                        // not a function call.
1124
                        return null;
48✔
1125
                }
1126

1127
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
162✔
1128
                        // If the variable is inside a different scope than the function name,
1129
                        // the function call doesn't apply to the variable.
1130
                        return null;
28✔
1131
                }
1132

1133
                return $functionPtr;
162✔
1134
        }
1135

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

1162
        /**
1163
         * @param File $phpcsFile
1164
         * @param int  $stackPtr
1165
         *
1166
         * @return bool
1167
         */
1168
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
1169
        {
1170
                $tokens = $phpcsFile->getTokens();
248✔
1171
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
248✔
1172

1173
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
248✔
1174
                if (! is_int($arrayPushOperatorIndex1)) {
248✔
1175
                        return false;
×
1176
                }
1177
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
248✔
1178
                        return false;
248✔
1179
                }
1180

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

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

1197
                return true;
28✔
1198
        }
1199

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

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

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

1255
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1256
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
312✔
1257
                        self::debug('found variable variable');
4✔
1258
                        return false;
4✔
1259
                }
1260
                return true;
312✔
1261
        }
1262

1263
        /**
1264
         * @param File $phpcsFile
1265
         * @param int  $stackPtr
1266
         *
1267
         * @return bool
1268
         */
1269
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
1270
        {
1271
                $tokens = $phpcsFile->getTokens();
312✔
1272

1273
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
312✔
1274
                if ($prev === false) {
312✔
1275
                        return false;
×
1276
                }
1277
                if ($tokens[$prev]['code'] === T_DOLLAR) {
312✔
1278
                        return true;
4✔
1279
                }
1280
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
312✔
1281
                        return false;
256✔
1282
                }
1283

1284
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
228✔
1285
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
228✔
1286
                        return true;
×
1287
                }
1288
                return false;
228✔
1289
        }
1290

1291
        /**
1292
         * @param File $phpcsFile
1293
         * @param int  $stackPtr
1294
         *
1295
         * @return EnumInfo|null
1296
         */
1297
        public static function makeEnumInfo(File $phpcsFile, $stackPtr)
1298
        {
1299
                $tokens = $phpcsFile->getTokens();
4✔
1300
                $token = $tokens[$stackPtr];
4✔
1301

1302
                if (isset($token['scope_opener'])) {
4✔
1303
                        $blockStart = $token['scope_opener'];
2✔
1304
                        $blockEnd = $token['scope_closer'];
2✔
1305
                } else {
1✔
1306
                        // Enums before phpcs could detect them do not have scopes so we have to
1307
                        // find them ourselves.
1308

1309
                        $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
4✔
1310
                        if (! is_int($blockStart)) {
4✔
1311
                                return null;
4✔
1312
                        }
1313
                        $blockEnd = $tokens[$blockStart]['bracket_closer'];
2✔
1314
                }
1315

1316
                return new EnumInfo(
4✔
1317
                        $stackPtr,
4✔
1318
                        $blockStart,
4✔
1319
                        $blockEnd
2✔
1320
                );
4✔
1321
        }
1322

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

1355
                $semicolonCount = 0;
8✔
1356
                $forLoopLevel = $tokens[$forIndex]['level'];
8✔
1357
                $forLoopNestedParensCount = 1;
8✔
1358

1359
                if (isset($tokens[$forIndex]['nested_parenthesis'])) {
8✔
1360
                        $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
×
1361
                }
1362

1363
                for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
8✔
1364
                        if ($tokens[$i]['code'] !== T_SEMICOLON) {
8✔
1365
                                continue;
8✔
1366
                        }
1367

1368
                        if ($tokens[$i]['level'] !== $forLoopLevel) {
8✔
1369
                                continue;
8✔
1370
                        }
1371

1372
                        if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
8✔
1373
                                continue;
×
1374
                        }
1375

1376
                        switch ($semicolonCount) {
1377
                                case 0:
8✔
1378
                                        $initEnd = $i;
8✔
1379
                                        $conditionStart = $initEnd + 1;
8✔
1380
                                        break;
8✔
1381
                                case 1:
8✔
1382
                                        $conditionEnd = $i;
8✔
1383
                                        $incrementStart = $conditionEnd + 1;
8✔
1384
                                        break;
8✔
1385
                        }
1386
                        $semicolonCount += 1;
8✔
1387
                }
4✔
1388

1389
                if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
8✔
1390
                        throw new \Exception("Cannot parse for loop at position {$forIndex}");
×
1391
                }
1392

1393
                return new ForLoopInfo(
8✔
1394
                        $forIndex,
8✔
1395
                        $blockStart,
8✔
1396
                        $blockEnd,
8✔
1397
                        $initStart,
8✔
1398
                        $initEnd,
8✔
1399
                        $conditionStart,
8✔
1400
                        $conditionEnd,
8✔
1401
                        $incrementStart,
8✔
1402
                        $incrementEnd
4✔
1403
                );
8✔
1404
        }
1405

1406
        /**
1407
         * @param int                     $stackPtr
1408
         * @param array<int, ForLoopInfo> $forLoops
1409
         * @return ForLoopInfo|null
1410
         */
1411
        public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
1412
        {
1413
                foreach ($forLoops as $forLoop) {
340✔
1414
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1415
                                return $forLoop;
8✔
1416
                        }
1417
                }
170✔
1418
                return null;
340✔
1419
        }
1420

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

1438
                $tokens = $phpcsFile->getTokens();
224✔
1439

1440
                // If the previous token is a visibility keyword, this is constructor
1441
                // promotion. eg: `public $foobar`.
1442
                $prevIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), $functionIndex, true);
224✔
1443
                if (! is_int($prevIndex)) {
224✔
1444
                        return false;
×
1445
                }
1446
                $prevToken = $tokens[$prevIndex];
224✔
1447
                if (in_array($prevToken['code'], Tokens::$scopeModifiers, true)) {
224✔
1448
                        return true;
8✔
1449
                }
1450

1451
                // If the previous token is not a visibility keyword, but the one before it
1452
                // is, the previous token was probably a typehint and this is constructor
1453
                // promotion. eg: `public boolean $foobar`.
1454
                $prev2Index = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevIndex - 1), $functionIndex, true);
224✔
1455
                if (! is_int($prev2Index)) {
224✔
1456
                        return false;
×
1457
                }
1458
                $prev2Token = $tokens[$prev2Index];
224✔
1459
                if (in_array($prev2Token['code'], Tokens::$scopeModifiers, true)) {
224✔
1460
                        return true;
12✔
1461
                }
1462

1463
                // If the previous token is not a visibility keyword, but the one two
1464
                // before it is, and one of the tokens is `readonly`, the previous token
1465
                // was probably a typehint and this is constructor promotion. eg: `public
1466
                // readonly boolean $foobar`.
1467
                $prev3Index = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev2Index - 1), $functionIndex, true);
220✔
1468
                if (! is_int($prev3Index)) {
220✔
1469
                        return false;
44✔
1470
                }
1471
                $prev3Token = $tokens[$prev3Index];
220✔
1472
                $wasPreviousReadonly = $prevToken['content'] === 'readonly' || $prev2Token['content'] === 'readonly';
220✔
1473
                if (in_array($prev3Token['code'], Tokens::$scopeModifiers, true) && $wasPreviousReadonly) {
220✔
1474
                        return true;
8✔
1475
                }
1476

1477
                return false;
220✔
1478
        }
1479

1480
        /**
1481
         * Return true if the token is inside an abstract class.
1482
         *
1483
         * @param File $phpcsFile
1484
         * @param int  $stackPtr
1485
         *
1486
         * @return bool
1487
         */
1488
        public static function isInAbstractClass(File $phpcsFile, $stackPtr)
1489
        {
1490
                $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
108✔
1491
                if (! is_int($classIndex)) {
108✔
1492
                        return false;
92✔
1493
                }
1494
                $classProperties = $phpcsFile->getClassProperties($classIndex);
16✔
1495
                return $classProperties['is_abstract'];
16✔
1496
        }
1497

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

© 2025 Coveralls, Inc