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

sirbrillig / phpcs-variable-analysis / 3847845700

pending completion
3847845700

Pull #287

github

GitHub
Merge 4c9cf2e1c into 6a0e2ffef
Pull Request #287: Composer: allow for the 1.0.0 version of the Composer PHPCS plugin

1449 of 1573 relevant lines covered (92.12%)

139.24 hits per line

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

84.91
/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\ScopeType;
9
use VariableAnalysis\Lib\VariableInfo;
10
use PHP_CodeSniffer\Util\Tokens;
11

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

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

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

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

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

81
        /**
82
         * @param (int|string)[] $conditions
83
         *
84
         * @return bool
85
         */
86
        public static function areAnyConditionsAClass(array $conditions)
87
        {
88
                foreach (array_reverse($conditions, true) as $scopeCode) {
28✔
89
                        if ($scopeCode === T_CLASS || $scopeCode === T_ANON_CLASS || $scopeCode === T_TRAIT) {
28✔
90
                                return true;
28✔
91
                        }
92
                }
14✔
93
                return false;
8✔
94
        }
95

96
        /**
97
         * Return true if the token conditions are within a function before they are
98
         * within a class.
99
         *
100
         * @param (int|string)[] $conditions
101
         *
102
         * @return bool
103
         */
104
        public static function areConditionsWithinFunctionBeforeClass(array $conditions)
105
        {
106
                $classTypes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
48✔
107
                foreach (array_reverse($conditions, true) as $scopeCode) {
48✔
108
                        if (in_array($scopeCode, $classTypes)) {
48✔
109
                                return false;
4✔
110
                        }
111
                        if ($scopeCode === T_FUNCTION) {
48✔
112
                                return true;
48✔
113
                        }
114
                }
12✔
115
                return false;
×
116
        }
117

118
        /**
119
         * Return true if the token conditions are within an if block before they are
120
         * within a class or function.
121
         *
122
         * @param (int|string)[] $conditions
123
         *
124
         * @return int|string|null
125
         */
126
        public static function getClosestIfPositionIfBeforeOtherConditions(array $conditions)
127
        {
128
                $conditionsInsideOut = array_reverse($conditions, true);
8✔
129
                if (empty($conditions)) {
8✔
130
                        return null;
×
131
                }
132
                $scopeCode = reset($conditionsInsideOut);
8✔
133
                if ($scopeCode === T_IF) {
8✔
134
                        return key($conditionsInsideOut);
8✔
135
                }
136
                return null;
8✔
137
        }
138

139
        /**
140
         * @param File $phpcsFile
141
         * @param int  $stackPtr
142
         *
143
         * @return bool
144
         */
145
        public static function isTokenFunctionParameter(File $phpcsFile, $stackPtr)
146
        {
147
                return is_int(self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr));
340✔
148
        }
149

150
        /**
151
         * Return true if the token is inside the arguments of a function call.
152
         *
153
         * For example, the variable `$foo` in `doSomething($foo)` is inside the
154
         * arguments to the call to `doSomething()`.
155
         *
156
         * @param File $phpcsFile
157
         * @param int  $stackPtr
158
         *
159
         * @return bool
160
         */
161
        public static function isTokenInsideFunctionCallArgument(File $phpcsFile, $stackPtr)
162
        {
163
                return is_int(self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr));
312✔
164
        }
165

166
        /**
167
         * Find the index of the function keyword for a token in a function
168
         * definition's parameters.
169
         *
170
         * Does not work for tokens inside the "use".
171
         *
172
         * Will also work for the parenthesis that make up the function definition's
173
         * parameters list.
174
         *
175
         * For arguments inside a function call, rather than a definition, use
176
         * `getFunctionIndexForFunctionCallArgument`.
177
         *
178
         * @param File $phpcsFile
179
         * @param int  $stackPtr
180
         *
181
         * @return ?int
182
         */
183
        public static function getFunctionIndexForFunctionParameter(File $phpcsFile, $stackPtr)
184
        {
185
                $tokens = $phpcsFile->getTokens();
340✔
186
                $token = $tokens[$stackPtr];
340✔
187
                if ($token['code'] === 'PHPCS_T_OPEN_PARENTHESIS') {
340✔
188
                        $startOfArguments = $stackPtr;
×
189
                } elseif ($token['code'] === 'PHPCS_T_CLOSE_PARENTHESIS') {
340✔
190
                        if (empty($token['parenthesis_opener'])) {
24✔
191
                                return null;
×
192
                        }
193
                        $startOfArguments = $token['parenthesis_opener'];
24✔
194
                } else {
12✔
195
                        if (empty($token['nested_parenthesis'])) {
340✔
196
                                return null;
336✔
197
                        }
198
                        $startingParenthesis = array_keys($token['nested_parenthesis']);
276✔
199
                        $startOfArguments = end($startingParenthesis);
276✔
200
                }
201

202
                if (! is_int($startOfArguments)) {
276✔
203
                        return null;
×
204
                }
205

206
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
276✔
207
                $nonFunctionTokenTypes[] = T_STRING;
276✔
208
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
276✔
209
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
276✔
210
                if (! is_int($functionPtr)) {
276✔
211
                        return null;
×
212
                }
213
                $functionToken = $tokens[$functionPtr];
276✔
214

215
                $functionTokenTypes = [
138✔
216
                        T_FUNCTION,
276✔
217
                        T_CLOSURE,
276✔
218
                ];
276✔
219
                if (!in_array($functionToken['code'], $functionTokenTypes, true) && ! self::isArrowFunction($phpcsFile, $functionPtr)) {
276✔
220
                        return null;
256✔
221
                }
222
                return $functionPtr;
224✔
223
        }
224

225
        /**
226
         * @param File $phpcsFile
227
         * @param int  $stackPtr
228
         *
229
         * @return bool
230
         */
231
        public static function isTokenInsideFunctionUseImport(File $phpcsFile, $stackPtr)
232
        {
233
                return is_int(self::getUseIndexForUseImport($phpcsFile, $stackPtr));
336✔
234
        }
235

236
        /**
237
         * Find the token index of the "use" for a token inside a function use import
238
         *
239
         * @param File $phpcsFile
240
         * @param int  $stackPtr
241
         *
242
         * @return ?int
243
         */
244
        public static function getUseIndexForUseImport(File $phpcsFile, $stackPtr)
245
        {
246
                $tokens = $phpcsFile->getTokens();
336✔
247

248
                $nonUseTokenTypes = Tokens::$emptyTokens;
336✔
249
                $nonUseTokenTypes[] = T_VARIABLE;
336✔
250
                $nonUseTokenTypes[] = T_ELLIPSIS;
336✔
251
                $nonUseTokenTypes[] = T_COMMA;
336✔
252
                $nonUseTokenTypes[] = T_BITWISE_AND;
336✔
253
                $openParenPtr = self::getIntOrNull($phpcsFile->findPrevious($nonUseTokenTypes, $stackPtr - 1, null, true, null, true));
336✔
254
                if (! is_int($openParenPtr) || $tokens[$openParenPtr]['code'] !== T_OPEN_PARENTHESIS) {
336✔
255
                        return null;
332✔
256
                }
257

258
                $usePtr = self::getIntOrNull($phpcsFile->findPrevious(array_values($nonUseTokenTypes), $openParenPtr - 1, null, true, null, true));
208✔
259
                if (! is_int($usePtr) || $tokens[$usePtr]['code'] !== T_USE) {
208✔
260
                        return null;
196✔
261
                }
262
                return $usePtr;
24✔
263
        }
264

265
        /**
266
         * Return the index of a function's name token from inside the function.
267
         *
268
         * $stackPtr must be inside the function body or parameters for this to work.
269
         *
270
         * @param File $phpcsFile
271
         * @param int  $stackPtr
272
         *
273
         * @return ?int
274
         */
275
        public static function findFunctionCall(File $phpcsFile, $stackPtr)
276
        {
277
                $tokens = $phpcsFile->getTokens();
316✔
278

279
                $openPtr = self::findContainingOpeningBracket($phpcsFile, $stackPtr);
316✔
280
                if (is_int($openPtr)) {
316✔
281
                        // First non-whitespace thing and see if it's a T_STRING function name
282
                        $functionPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
204✔
283
                        if (is_int($functionPtr) && $tokens[$functionPtr]['code'] === T_STRING) {
204✔
284
                                return $functionPtr;
152✔
285
                        }
286
                }
66✔
287
                return null;
304✔
288
        }
289

290
        /**
291
         * @param File $phpcsFile
292
         * @param int  $stackPtr
293
         *
294
         * @return array<int, array<int>>
295
         */
296
        public static function findFunctionCallArguments(File $phpcsFile, $stackPtr)
297
        {
298
                $tokens = $phpcsFile->getTokens();
32✔
299

300
                // Slight hack: also allow this to find args for array constructor.
301
                if (($tokens[$stackPtr]['code'] !== T_STRING) && ($tokens[$stackPtr]['code'] !== T_ARRAY)) {
32✔
302
                        // Assume $stackPtr is something within the brackets, find our function call
303
                        $stackPtr = self::findFunctionCall($phpcsFile, $stackPtr);
20✔
304
                        if ($stackPtr === null) {
20✔
305
                                return [];
×
306
                        }
307
                }
10✔
308

309
                // $stackPtr is the function name, find our brackets after it
310
                $openPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
32✔
311
                if (($openPtr === false) || ($tokens[$openPtr]['code'] !== T_OPEN_PARENTHESIS)) {
32✔
312
                        return [];
×
313
                }
314

315
                if (!isset($tokens[$openPtr]['parenthesis_closer'])) {
32✔
316
                        return [];
×
317
                }
318
                $closePtr = $tokens[$openPtr]['parenthesis_closer'];
32✔
319

320
                $argPtrs = [];
32✔
321
                $lastPtr = $openPtr;
32✔
322
                $lastArgComma = $openPtr;
32✔
323
                $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
32✔
324
                while (is_int($nextPtr)) {
32✔
325
                        if (self::findContainingOpeningBracket($phpcsFile, $nextPtr) === $openPtr) {
32✔
326
                                // Comma is at our level of brackets, it's an argument delimiter.
327
                                $range = range($lastArgComma + 1, $nextPtr - 1);
32✔
328
                                $range = array_filter($range, function ($element) {
16✔
329
                                        return is_int($element);
32✔
330
                                });
32✔
331
                                array_push($argPtrs, $range);
32✔
332
                                $lastArgComma = $nextPtr;
32✔
333
                        }
16✔
334
                        $lastPtr = $nextPtr;
32✔
335
                        $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
32✔
336
                }
16✔
337
                $range = range($lastArgComma + 1, $closePtr - 1);
32✔
338
                $range = array_filter($range, function ($element) {
16✔
339
                        return is_int($element);
32✔
340
                });
32✔
341
                array_push($argPtrs, $range);
32✔
342

343
                return $argPtrs;
32✔
344
        }
345

346
        /**
347
         * @param File $phpcsFile
348
         * @param int  $stackPtr
349
         *
350
         * @return ?int
351
         */
352
        public static function getNextAssignPointer(File $phpcsFile, $stackPtr)
353
        {
354
                $tokens = $phpcsFile->getTokens();
328✔
355

356
                // Is the next non-whitespace an assignment?
357
                $nextPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
328✔
358
                if (is_int($nextPtr)
328✔
359
                        && isset(Tokens::$assignmentTokens[$tokens[$nextPtr]['code']])
328✔
360
                        // Ignore double arrow to prevent triggering on `foreach ( $array as $k => $v )`.
361
                        && $tokens[$nextPtr]['code'] !== T_DOUBLE_ARROW
328✔
362
                ) {
164✔
363
                        return $nextPtr;
312✔
364
                }
365
                return null;
320✔
366
        }
367

368
        /**
369
         * @param string $varName
370
         *
371
         * @return string
372
         */
373
        public static function normalizeVarName($varName)
374
        {
375
                $result = preg_replace('/[{}$]/', '', $varName);
340✔
376
                return $result ? $result : $varName;
340✔
377
        }
378

379
        /**
380
         * @param File   $phpcsFile
381
         * @param int    $stackPtr
382
         * @param string $varName   (optional) if it differs from the normalized 'content' of the token at $stackPtr
383
         *
384
         * @return ?int
385
         */
386
        public static function findVariableScope(File $phpcsFile, $stackPtr, $varName = null)
387
        {
388
                $tokens = $phpcsFile->getTokens();
340✔
389
                $token = $tokens[$stackPtr];
340✔
390
                $varName = isset($varName) ? $varName : self::normalizeVarName($token['content']);
340✔
391

392
                $arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr);
340✔
393
                $isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex);
340✔
394
                if ($isTokenInsideArrowFunctionBody) {
340✔
395
                        // Get the list of variables defined by the arrow function
396
                        // If this matches any of them, the scope is the arrow function,
397
                        // otherwise, it uses the enclosing scope.
398
                        if ($arrowFunctionIndex) {
24✔
399
                                $variableNames = self::getVariablesDefinedByArrowFunction($phpcsFile, $arrowFunctionIndex);
24✔
400
                                self::debug('findVariableScope: looking for', $varName, 'in arrow function variables', $variableNames);
24✔
401
                                if (in_array($varName, $variableNames, true)) {
24✔
402
                                        return $arrowFunctionIndex;
24✔
403
                                }
404
                        }
8✔
405
                }
8✔
406

407
                return self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
340✔
408
        }
409

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

451
                $startOfTokenScope = self::getStartOfTokenScope($phpcsFile, $stackPtr);
340✔
452
                if (is_int($startOfTokenScope) && $startOfTokenScope > 0) {
340✔
453
                        return $startOfTokenScope;
328✔
454
                }
455

456
                // If there is no "conditions" array, this is a function definition argument.
457
                if (self::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
272✔
458
                        $functionPtr = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
220✔
459
                        if (! is_int($functionPtr)) {
220✔
460
                                throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
461
                        }
462
                        return $functionPtr;
220✔
463
                }
464

465
                self::debug('Cannot find function scope for variable at', $stackPtr);
108✔
466
                return $startOfTokenScope;
108✔
467
        }
468

469
        /**
470
         * Return the token index of the scope start for a variable token
471
         *
472
         * This will only work for a variable within a function's body. Otherwise,
473
         * see `findVariableScope`, which is more complex.
474
         *
475
         * Note that if used on a variable in an arrow function, it will return the
476
         * enclosing function's scope, which may be incorrect.
477
         *
478
         * @param File $phpcsFile
479
         * @param int  $stackPtr
480
         *
481
         * @return ?int
482
         */
483
        private static function getStartOfTokenScope(File $phpcsFile, $stackPtr)
484
        {
485
                $tokens = $phpcsFile->getTokens();
340✔
486
                $token = $tokens[$stackPtr];
340✔
487

488
                $inClass = false;
340✔
489
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
340✔
490
                $functionTokenTypes = [
170✔
491
                        T_FUNCTION,
340✔
492
                        T_CLOSURE,
340✔
493
                ];
340✔
494
                foreach (array_reverse($conditions, true) as $scopePtr => $scopeCode) {
340✔
495
                        if (in_array($scopeCode, $functionTokenTypes, true) || self::isArrowFunction($phpcsFile, $scopePtr)) {
332✔
496
                                return $scopePtr;
328✔
497
                        }
498
                        if (isset(Tokens::$ooScopeTokens[$scopeCode]) === true) {
148✔
499
                                $inClass = true;
56✔
500
                        }
28✔
501
                }
154✔
502

503
                if ($inClass) {
272✔
504
                        // If this is inside a class and not inside a function, this is either a
505
                        // class member variable definition, or a function argument. If it is a
506
                        // variable definition, it has no scope on its own (it can only be used
507
                        // with an object reference). If it is a function argument, we need to do
508
                        // more work (see `findVariableScopeExceptArrowFunctions`).
509
                        return null;
56✔
510
                }
511

512
                // If we can't find a scope, let's use the first token of the file.
513
                return 0;
236✔
514
        }
515

516
        /**
517
         * @param File $phpcsFile
518
         * @param int  $stackPtr
519
         *
520
         * @return bool
521
         */
522
        public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $stackPtr)
523
        {
524
                $tokens = $phpcsFile->getTokens();
×
525
                $token = $tokens[$stackPtr];
×
526
                $openParenIndices = isset($token['nested_parenthesis']) ? $token['nested_parenthesis'] : [];
×
527
                if (empty($openParenIndices)) {
×
528
                        return false;
×
529
                }
530
                $openParenPtr = $openParenIndices[0];
×
531
                return self::isArrowFunction($phpcsFile, $openParenPtr - 1);
×
532
        }
533

534
        /**
535
         * @param File $phpcsFile
536
         * @param int  $stackPtr
537
         *
538
         * @return ?int
539
         */
540
        public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr)
541
        {
542
                $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr);
340✔
543
                if (! is_int($arrowFunctionIndex)) {
340✔
544
                        return null;
340✔
545
                }
546
                $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex);
24✔
547
                if (! $arrowFunctionInfo) {
24✔
548
                        return null;
×
549
                }
550
                $arrowFunctionScopeStart = $arrowFunctionInfo['scope_opener'];
24✔
551
                $arrowFunctionScopeEnd = $arrowFunctionInfo['scope_closer'];
24✔
552
                if ($stackPtr > $arrowFunctionScopeStart && $stackPtr < $arrowFunctionScopeEnd) {
24✔
553
                        return $arrowFunctionIndex;
24✔
554
                }
555
                return null;
24✔
556
        }
557

558
        /**
559
         * @param File $phpcsFile
560
         * @param int  $stackPtr
561
         *
562
         * @return ?int
563
         */
564
        private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr)
565
        {
566
                $tokens = $phpcsFile->getTokens();
340✔
567
                $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
340✔
568
                for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) {
340✔
569
                        $token = $tokens[$index];
340✔
570
                        if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) {
340✔
571
                                return $index;
24✔
572
                        }
573
                }
170✔
574
                return null;
340✔
575
        }
576

577
        /**
578
         * @param File $phpcsFile
579
         * @param int  $stackPtr
580
         *
581
         * @return bool
582
         */
583
        public static function isArrowFunction(File $phpcsFile, $stackPtr)
584
        {
585
                $tokens = $phpcsFile->getTokens();
340✔
586
                if (defined('T_FN') && $tokens[$stackPtr]['code'] === T_FN) {
340✔
587
                        return true;
24✔
588
                }
589
                if ($tokens[$stackPtr]['content'] !== 'fn') {
340✔
590
                        return false;
340✔
591
                }
592
                // Make sure next non-space token is an open parenthesis
593
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
×
594
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
×
595
                        return false;
×
596
                }
597
                // Find the associated close parenthesis
598
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
×
599
                // Make sure the next token is a fat arrow
600
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
×
601
                if (! is_int($fatArrowIndex)) {
×
602
                        return false;
×
603
                }
604
                if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') {
×
605
                        return false;
×
606
                }
607
                return true;
×
608
        }
609

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

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

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

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

716
                return false;
28✔
717
        }
718

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

737
                // First find the end of the list
738
                $closePtr = null;
52✔
739
                if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) {
52✔
740
                        $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer'];
32✔
741
                }
16✔
742
                if (isset($tokens[$listOpenerIndex]['bracket_closer'])) {
52✔
743
                        $closePtr = $tokens[$listOpenerIndex]['bracket_closer'];
52✔
744
                }
26✔
745
                if (! $closePtr) {
52✔
746
                        return null;
×
747
                }
748

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

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

769
                        // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion)
770
                        $isNestedAssignment = null;
12✔
771
                        $parentListOpener = array_keys(array_reverse($parents, true))[0];
12✔
772
                        $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener);
12✔
773
                        if ($isNestedAssignment === null) {
12✔
774
                                return null;
8✔
775
                        }
776
                }
2✔
777

778
                $variablePtrs = [];
44✔
779

780
                $currentPtr = $listOpenerIndex;
44✔
781
                $variablePtr = 0;
44✔
782
                while ($currentPtr < $closePtr && is_int($variablePtr)) {
44✔
783
                        $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr);
44✔
784
                        if (is_int($variablePtr)) {
44✔
785
                                $variablePtrs[] = $variablePtr;
32✔
786
                        }
16✔
787
                        ++$currentPtr;
44✔
788
                }
22✔
789

790
                if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) {
44✔
791
                        return null;
28✔
792
                }
793

794
                return $variablePtrs;
32✔
795
        }
796

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

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

843
        /**
844
         * @param string $pattern
845
         * @param string $value
846
         *
847
         * @return string[]
848
         */
849
        public static function splitStringToArray($pattern, $value)
850
        {
851
                $result = preg_split($pattern, $value);
24✔
852
                return is_array($result) ? $result : [];
24✔
853
        }
854

855
        /**
856
         * @param string $varName
857
         *
858
         * @return bool
859
         */
860
        public static function isVariableANumericVariable($varName)
861
        {
862
                return is_numeric(substr($varName, 0, 1));
324✔
863
        }
864

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

894
        /**
895
         * @param File $phpcsFile
896
         * @param int  $stackPtr
897
         *
898
         * @return bool
899
         */
900
        public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr)
901
        {
902
                $tokens = $phpcsFile->getTokens();
316✔
903
                $token = $tokens[$stackPtr];
316✔
904
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
316✔
905
                $elseTokenTypes = [
158✔
906
                        T_ELSE,
316✔
907
                        T_ELSEIF,
316✔
908
                ];
316✔
909
                foreach (array_reverse($conditions, true) as $scopeCode) {
316✔
910
                        if (in_array($scopeCode, $elseTokenTypes, true)) {
304✔
911
                                return true;
8✔
912
                        }
913
                }
158✔
914

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

926
                return false;
316✔
927
        }
928

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

942
                $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
16✔
943
                if (! is_int($ifPtr)) {
16✔
944
                        throw new \Exception("Cannot find if for else at {$stackPtr}");
×
945
                }
946
                $blockIndices = [$ifPtr];
16✔
947

948
                $previousElseIfPtr = $currentElsePtr;
16✔
949
                do {
950
                        $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
16✔
951
                        if (is_int($elseIfPtr)) {
16✔
952
                                $blockIndices[] = $elseIfPtr;
16✔
953
                                $previousElseIfPtr = $elseIfPtr;
16✔
954
                        }
8✔
955
                } while (is_int($elseIfPtr));
16✔
956

957
                return $blockIndices;
16✔
958
        }
959

960
        /**
961
         * @param int $needle
962
         * @param int $scopeStart
963
         * @param int $scopeEnd
964
         *
965
         * @return bool
966
         */
967
        public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
968
        {
969
                return ($needle > $scopeStart && $needle < $scopeEnd);
16✔
970
        }
971

972
        /**
973
         * @param File $phpcsFile
974
         * @param int  $scopeStartIndex
975
         *
976
         * @return int
977
         */
978
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
979
        {
980
                $tokens = $phpcsFile->getTokens();
340✔
981
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
340✔
982

983
                if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) {
340✔
984
                        $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex);
24✔
985
                        $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex;
24✔
986
                }
12✔
987

988
                if ($scopeStartIndex === 0) {
340✔
989
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
340✔
990
                }
170✔
991
                return $scopeCloserIndex;
340✔
992
        }
993

994
        /**
995
         * @param File $phpcsFile
996
         *
997
         * @return int
998
         */
999
        public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
1000
        {
1001
                $tokens = $phpcsFile->getTokens();
340✔
1002
                foreach (array_reverse($tokens, true) as $index => $token) {
340✔
1003
                        if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
340✔
1004
                                return $index;
340✔
1005
                        }
1006
                }
166✔
1007
                self::debug('no non-empty token found for end of file');
×
1008
                return 0;
×
1009
        }
1010

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

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

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

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

1114
                $functionNameType = $tokens[$functionPtr]['code'];
164✔
1115
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
164✔
1116
                        // If the alleged function name is not a variable or a string, this is
1117
                        // not a function call.
1118
                        return null;
48✔
1119
                }
1120

1121
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
160✔
1122
                        // If the variable is inside a different scope than the function name,
1123
                        // the function call doesn't apply to the variable.
1124
                        return null;
28✔
1125
                }
1126

1127
                return $functionPtr;
160✔
1128
        }
1129

1130
        /**
1131
         * @param File $phpcsFile
1132
         * @param int  $stackPtr
1133
         *
1134
         * @return bool
1135
         */
1136
        public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
1137
        {
1138
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
316✔
1139
                if (! is_int($functionIndex)) {
316✔
1140
                        return false;
300✔
1141
                }
1142
                $tokens = $phpcsFile->getTokens();
160✔
1143
                if (! isset($tokens[$functionIndex])) {
160✔
1144
                        return false;
×
1145
                }
1146
                $allowedFunctionNames = [
80✔
1147
                        'isset',
160✔
1148
                        'empty',
160✔
1149
                ];
160✔
1150
                if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) {
160✔
1151
                        return true;
4✔
1152
                }
1153
                return false;
160✔
1154
        }
1155

1156
        /**
1157
         * @param File $phpcsFile
1158
         * @param int  $stackPtr
1159
         *
1160
         * @return bool
1161
         */
1162
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
1163
        {
1164
                $tokens = $phpcsFile->getTokens();
244✔
1165
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
244✔
1166

1167
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
244✔
1168
                if (! is_int($arrayPushOperatorIndex1)) {
244✔
1169
                        return false;
×
1170
                }
1171
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
244✔
1172
                        return false;
244✔
1173
                }
1174

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

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

1191
                return true;
28✔
1192
        }
1193

1194
        /**
1195
         * @param File $phpcsFile
1196
         * @param int  $stackPtr
1197
         *
1198
         * @return bool
1199
         */
1200
        public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
1201
        {
1202
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
244✔
1203
                if (! is_int($functionIndex)) {
244✔
1204
                        return false;
216✔
1205
                }
1206
                $tokens = $phpcsFile->getTokens();
96✔
1207
                if (! isset($tokens[$functionIndex])) {
96✔
1208
                        return false;
×
1209
                }
1210
                if ($tokens[$functionIndex]['content'] === 'unset') {
96✔
1211
                        return true;
8✔
1212
                }
1213
                return false;
88✔
1214
        }
1215

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

1235
        /**
1236
         * @param File $phpcsFile
1237
         * @param int  $stackPtr
1238
         *
1239
         * @return bool
1240
         */
1241
        public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
1242
        {
1243
                // Is the next non-whitespace an assignment?
1244
                $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
328✔
1245
                if (! is_int($assignPtr)) {
328✔
1246
                        return false;
320✔
1247
                }
1248

1249
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1250
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
312✔
1251
                        self::debug('found variable variable');
4✔
1252
                        return false;
4✔
1253
                }
1254
                return true;
312✔
1255
        }
1256

1257
        /**
1258
         * @param File $phpcsFile
1259
         * @param int  $stackPtr
1260
         *
1261
         * @return bool
1262
         */
1263
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
1264
        {
1265
                $tokens = $phpcsFile->getTokens();
312✔
1266

1267
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
312✔
1268
                if ($prev === false) {
312✔
1269
                        return false;
×
1270
                }
1271
                if ($tokens[$prev]['code'] === T_DOLLAR) {
312✔
1272
                        return true;
4✔
1273
                }
1274
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
312✔
1275
                        return false;
256✔
1276
                }
1277

1278
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
228✔
1279
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
228✔
1280
                        return true;
×
1281
                }
1282
                return false;
228✔
1283
        }
1284

1285
        /**
1286
         * @param File $phpcsFile
1287
         * @param int  $stackPtr
1288
         *
1289
         * @return ForLoopInfo
1290
         */
1291
        public static function makeForLoopInfo(File $phpcsFile, $stackPtr)
1292
        {
1293
                $tokens = $phpcsFile->getTokens();
8✔
1294
                $token = $tokens[$stackPtr];
8✔
1295
                $forIndex = $stackPtr;
8✔
1296
                $blockStart = $token['parenthesis_closer'];
8✔
1297
                if (isset($token['scope_opener'])) {
8✔
1298
                        $blockStart = $token['scope_opener'];
8✔
1299
                        $blockEnd = $token['scope_closer'];
8✔
1300
                } else {
4✔
1301
                        // Some for loop blocks will not have scope positions because it they are
1302
                        // inline (no curly braces) so we have to find the end of their scope by
1303
                        // looking for the end of the next statement.
1304
                        $nextSemicolonIndex = $phpcsFile->findNext([T_SEMICOLON], $token['parenthesis_closer']);
8✔
1305
                        if (! is_int($nextSemicolonIndex)) {
8✔
1306
                                $nextSemicolonIndex = $token['parenthesis_closer'] + 1;
×
1307
                        }
1308
                        $blockEnd = $nextSemicolonIndex;
8✔
1309
                }
1310
                $initStart = intval($token['parenthesis_opener']) + 1;
8✔
1311
                $initEnd = null;
8✔
1312
                $conditionStart = null;
8✔
1313
                $conditionEnd = null;
8✔
1314
                $incrementStart = null;
8✔
1315
                $incrementEnd = $token['parenthesis_closer'] - 1;
8✔
1316

1317
                $semicolonCount = 0;
8✔
1318
                $forLoopLevel = $tokens[$forIndex]['level'];
8✔
1319
                $forLoopNestedParensCount = 1;
8✔
1320

1321
                if (isset($tokens[$forIndex]['nested_parenthesis'])) {
8✔
1322
                        $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
×
1323
                }
1324

1325
                for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
8✔
1326
                        if ($tokens[$i]['code'] !== T_SEMICOLON) {
8✔
1327
                                continue;
8✔
1328
                        }
1329

1330
                        if ($tokens[$i]['level'] !== $forLoopLevel) {
8✔
1331
                                continue;
8✔
1332
                        }
1333

1334
                        if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
8✔
1335
                                continue;
×
1336
                        }
1337

1338
                        switch ($semicolonCount) {
1339
                                case 0:
8✔
1340
                                        $initEnd = $i;
8✔
1341
                                        $conditionStart = $initEnd + 1;
8✔
1342
                                        break;
8✔
1343
                                case 1:
8✔
1344
                                        $conditionEnd = $i;
8✔
1345
                                        $incrementStart = $conditionEnd + 1;
8✔
1346
                                        break;
8✔
1347
                        }
1348
                        $semicolonCount += 1;
8✔
1349
                }
4✔
1350

1351
                if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
8✔
1352
                        throw new \Exception("Cannot parse for loop at position {$forIndex}");
×
1353
                }
1354

1355
                return new ForLoopInfo(
8✔
1356
                        $forIndex,
8✔
1357
                        $blockStart,
8✔
1358
                        $blockEnd,
8✔
1359
                        $initStart,
8✔
1360
                        $initEnd,
8✔
1361
                        $conditionStart,
8✔
1362
                        $conditionEnd,
8✔
1363
                        $incrementStart,
8✔
1364
                        $incrementEnd
4✔
1365
                );
8✔
1366
        }
1367

1368
        /**
1369
         * @param int                     $stackPtr
1370
         * @param array<int, ForLoopInfo> $forLoops
1371
         * @return ForLoopInfo|null
1372
         */
1373
        public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
1374
        {
1375
                foreach ($forLoops as $forLoop) {
336✔
1376
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1377
                                return $forLoop;
8✔
1378
                        }
1379
                }
168✔
1380
                return null;
336✔
1381
        }
1382

1383
        /**
1384
         * Return true if the token looks like constructor promotion.
1385
         *
1386
         * Call on a parameter variable token only.
1387
         *
1388
         * @param File $phpcsFile
1389
         * @param int  $stackPtr
1390
         *
1391
         * @return bool
1392
         */
1393
        public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
1394
        {
1395
                $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
220✔
1396
                if (! $functionIndex) {
220✔
1397
                        return false;
×
1398
                }
1399

1400
                $tokens = $phpcsFile->getTokens();
220✔
1401

1402
                // If the previous token is a visibility keyword, this is constructor
1403
                // promotion. eg: `public $foobar`.
1404
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), $functionIndex, true);
220✔
1405
                if (! is_int($prev)) {
220✔
1406
                        return false;
×
1407
                }
1408
                $prevToken = $tokens[$prev];
220✔
1409
                if (in_array($prevToken['code'], Tokens::$scopeModifiers, true)) {
220✔
1410
                        return true;
8✔
1411
                }
1412

1413
                // If the previous token is not a visibility keyword, but the one before it
1414
                // is, the previous token was probably a typehint and this is constructor
1415
                // promotion. eg: `public boolean $foobar`.
1416
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), $functionIndex, true);
220✔
1417
                if (! is_int($prev)) {
220✔
1418
                        return false;
×
1419
                }
1420
                $prevToken = $tokens[$prev];
220✔
1421
                if (in_array($prevToken['code'], Tokens::$scopeModifiers, true)) {
220✔
1422
                        return true;
8✔
1423
                }
1424

1425
                return false;
220✔
1426
        }
1427

1428
        /**
1429
         * Return true if the token is inside an abstract class.
1430
         *
1431
         * @param File $phpcsFile
1432
         * @param int  $stackPtr
1433
         *
1434
         * @return bool
1435
         */
1436
        public static function isInAbstractClass(File $phpcsFile, $stackPtr)
1437
        {
1438
                $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
108✔
1439
                if (! is_int($classIndex)) {
108✔
1440
                        return false;
92✔
1441
                }
1442
                $classProperties = $phpcsFile->getClassProperties($classIndex);
16✔
1443
                return $classProperties['is_abstract'];
16✔
1444
        }
1445

1446
        /**
1447
         * Return true if the function body is empty or contains only `return;`
1448
         *
1449
         * @param File $phpcsFile
1450
         * @param int  $stackPtr  The index of the function keyword.
1451
         *
1452
         * @return bool
1453
         */
1454
        public static function isFunctionBodyEmpty(File $phpcsFile, $stackPtr)
1455
        {
1456
                $tokens = $phpcsFile->getTokens();
8✔
1457
                if ($tokens[$stackPtr]['code'] !== T_FUNCTION) {
8✔
1458
                        return false;
×
1459
                }
1460
                $functionScopeStart = $tokens[$stackPtr]['scope_opener'];
8✔
1461
                $functionScopeEnd = $tokens[$stackPtr]['scope_closer'];
8✔
1462
                $tokensToIgnore = array_merge(
8✔
1463
                        Tokens::$emptyTokens,
8✔
1464
                        [
4✔
1465
                                T_RETURN,
8✔
1466
                                T_SEMICOLON,
8✔
1467
                                T_OPEN_CURLY_BRACKET,
8✔
1468
                                T_CLOSE_CURLY_BRACKET,
8✔
1469
                        ]
4✔
1470
                );
8✔
1471
                for ($i = $functionScopeStart; $i < $functionScopeEnd; $i++) {
8✔
1472
                        if (! in_array($tokens[$i]['code'], $tokensToIgnore, true)) {
8✔
1473
                                return false;
8✔
1474
                        }
1475
                }
4✔
1476
                return true;
8✔
1477
        }
1478
}
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