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

sirbrillig / phpcs-variable-analysis / 21342792200

26 Jan 2026 12:57AM UTC coverage: 94.314% (+0.6%) from 93.732%
21342792200

Pull #360

github

sirbrillig
Remove unnecessary psalm disables
Pull Request #360: Migrate to PHPCSUtils

29 of 31 new or added lines in 2 files covered. (93.55%)

6 existing lines in 2 files now uncovered.

1808 of 1917 relevant lines covered (94.31%)

137.86 hits per line

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

91.52
/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\Constants;
8
use VariableAnalysis\Lib\ForLoopInfo;
9
use VariableAnalysis\Lib\EnumInfo;
10
use VariableAnalysis\Lib\ScopeType;
11
use VariableAnalysis\Lib\VariableInfo;
12
use PHP_CodeSniffer\Util\Tokens;
13
use PHPCSUtils\Utils\Context;
14
use PHPCSUtils\Utils\Lists;
15
use PHPCSUtils\Utils\Parentheses;
16

17
class Helpers
18
{
19
        /**
20
         * @return array<int|string>
21
         */
22
        public static function getPossibleEndOfFileTokens()
356✔
23
        {
24
                return array_merge(
356✔
25
                        array_values(Tokens::$emptyTokens),
356✔
26
                        [
178✔
27
                                T_INLINE_HTML,
356✔
28
                                T_CLOSE_TAG,
356✔
29
                        ]
178✔
30
                );
267✔
31
        }
32

33
        /**
34
         * @param int|bool $value
35
         *
36
         * @return ?int
37
         */
38
        public static function getIntOrNull($value)
352✔
39
        {
40
                return is_int($value) ? $value : null;
352✔
41
        }
42

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

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

88
        /**
89
         * @param File $phpcsFile
90
         * @param int  $stackPtr
91
         *
92
         * @return ?int
93
         */
94
        public static function findContainingOpeningBracket(File $phpcsFile, $stackPtr)
352✔
95
        {
96
                // Use PHPCSUtils to get the innermost parenthesis opener
97
                $result = Parentheses::getLastOpener($phpcsFile, $stackPtr);
352✔
98

99
                // PHPCSUtils returns false on failure, but our code expects null
100
                return $result !== false ? $result : null;
352✔
101
        }
102

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

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

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

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

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

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

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

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

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

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

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

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

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

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

317
                $openPtr = self::findContainingOpeningBracket($phpcsFile, $stackPtr);
332✔
318
                if (is_int($openPtr)) {
332✔
319
                        // First non-whitespace thing and see if it's a T_STRING function name
320
                        $functionPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
216✔
321
                        if (is_int($functionPtr)) {
216✔
322
                                $functionTokenCode = $tokens[$functionPtr]['code'];
216✔
323
                                // In PHPCS 4.x, function names can be T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, or T_NAME_RELATIVE
324
                                $validFunctionTokens = [
108✔
325
                                        T_STRING,
216✔
326
                                        T_NAME_FULLY_QUALIFIED,
216✔
327
                                        T_NAME_QUALIFIED,
216✔
328
                                        T_NAME_RELATIVE,
216✔
329
                                ];
162✔
330
                                if (in_array($functionTokenCode, $validFunctionTokens, true)) {
216✔
331
                                        return $functionPtr;
160✔
332
                                }
333
                        }
37✔
334
                }
37✔
335
                return null;
320✔
336
        }
337

338
        /**
339
         * @param File $phpcsFile
340
         * @param int  $stackPtr
341
         *
342
         * @return array<int, array<int>>
343
         */
344
        public static function findFunctionCallArguments(File $phpcsFile, $stackPtr)
36✔
345
        {
346
                $tokens = $phpcsFile->getTokens();
36✔
347

348
                // Slight hack: also allow this to find args for array constructor.
349
                if (($tokens[$stackPtr]['code'] !== T_STRING) && ($tokens[$stackPtr]['code'] !== T_ARRAY)) {
36✔
350
                        // Assume $stackPtr is something within the brackets, find our function call
351
                        $stackPtr = self::findFunctionCall($phpcsFile, $stackPtr);
24✔
352
                        if ($stackPtr === null) {
24✔
353
                                return [];
×
354
                        }
355
                }
6✔
356

357
                // $stackPtr is the function name, find our brackets after it
358
                $openPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
36✔
359
                if (($openPtr === false) || ($tokens[$openPtr]['code'] !== T_OPEN_PARENTHESIS)) {
36✔
360
                        return [];
×
361
                }
362

363
                if (!isset($tokens[$openPtr]['parenthesis_closer'])) {
36✔
364
                        return [];
×
365
                }
366
                $closePtr = $tokens[$openPtr]['parenthesis_closer'];
36✔
367

368
                $argPtrs = [];
36✔
369
                $lastPtr = $openPtr;
36✔
370
                $lastArgComma = $openPtr;
36✔
371
                $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
36✔
372
                while (is_int($nextPtr)) {
36✔
373
                        if (self::findContainingOpeningBracket($phpcsFile, $nextPtr) === $openPtr) {
36✔
374
                                // Comma is at our level of brackets, it's an argument delimiter.
375
                                $range = range($lastArgComma + 1, $nextPtr - 1);
36✔
376
                                array_push($argPtrs, $range);
36✔
377
                                $lastArgComma = $nextPtr;
36✔
378
                        }
9✔
379
                        $lastPtr = $nextPtr;
36✔
380
                        $nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
36✔
381
                }
9✔
382
                $range = range($lastArgComma + 1, $closePtr - 1);
36✔
383
                $range = array_filter($range, function ($element) {
18✔
384
                        return is_int($element);
36✔
385
                });
36✔
386
                array_push($argPtrs, $range);
36✔
387

388
                return $argPtrs;
36✔
389
        }
390

391
        /**
392
         * @param File $phpcsFile
393
         * @param int  $stackPtr
394
         *
395
         * @return ?int
396
         */
397
        public static function getNextAssignPointer(File $phpcsFile, $stackPtr)
344✔
398
        {
399
                $tokens = $phpcsFile->getTokens();
344✔
400

401
                // Is the next non-whitespace an assignment?
402
                $nextPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
344✔
403
                if (
404
                        is_int($nextPtr)
344✔
405
                        && isset(Tokens::$assignmentTokens[$tokens[$nextPtr]['code']])
344✔
406
                        // Ignore double arrow to prevent triggering on `foreach ( $array as $k => $v )`.
407
                        && $tokens[$nextPtr]['code'] !== T_DOUBLE_ARROW
344✔
408
                ) {
86✔
409
                        return $nextPtr;
324✔
410
                }
411
                return null;
336✔
412
        }
413

414
        /**
415
         * @param string $varName
416
         *
417
         * @return string
418
         */
419
        public static function normalizeVarName($varName)
356✔
420
        {
421
                $result = preg_replace('/[{}$]/', '', $varName);
356✔
422
                return $result ? $result : $varName;
356✔
423
        }
424

425
        /**
426
         * @param File   $phpcsFile
427
         * @param int    $stackPtr
428
         * @param string $varName   (optional) if it differs from the normalized 'content' of the token at $stackPtr
429
         *
430
         * @return ?int
431
         */
432
        public static function findVariableScope(File $phpcsFile, $stackPtr, $varName = null)
356✔
433
        {
434
                $tokens = $phpcsFile->getTokens();
356✔
435
                $token = $tokens[$stackPtr];
356✔
436
                $varName = isset($varName) ? $varName : self::normalizeVarName($token['content']);
356✔
437

438
                $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
356✔
439

440
                if (!is_null($enclosingScopeIndex)) {
356✔
441
                        $arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
352✔
442
                        $isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex);
352✔
443
                        if ($isTokenInsideArrowFunctionBody) {
352✔
444
                                // Get the list of variables defined by the arrow function
445
                                // If this matches any of them, the scope is the arrow function,
446
                                // otherwise, it uses the enclosing scope.
447
                                if ($arrowFunctionIndex) {
36✔
448
                                        $variableNames = self::getVariablesDefinedByArrowFunction($phpcsFile, $arrowFunctionIndex);
36✔
449
                                        self::debug('findVariableScope: looking for', $varName, 'in arrow function variables', $variableNames);
36✔
450
                                        if (in_array($varName, $variableNames, true)) {
36✔
451
                                                return $arrowFunctionIndex;
36✔
452
                                        }
453
                                }
7✔
454
                        }
7✔
455
                }
88✔
456

457
                return $enclosingScopeIndex;
356✔
458
        }
459

460
        /**
461
         * Return the variable names and positions of each variable targetted by a `compact()` call.
462
         *
463
         * @param File                   $phpcsFile
464
         * @param int                    $stackPtr
465
         * @param array<int, array<int>> $arguments The stack pointers of each argument; see findFunctionCallArguments
466
         *
467
         * @return array<VariableInfo> each variable's firstRead position and its name; other VariableInfo properties are not set!
468
         */
469
        public static function getVariablesInsideCompact(File $phpcsFile, $stackPtr, $arguments)
12✔
470
        {
471
                $tokens = $phpcsFile->getTokens();
12✔
472
                $variablePositionsAndNames = [];
12✔
473

474
                foreach ($arguments as $argumentPtrs) {
12✔
475
                        $argumentPtrs = array_values(array_filter($argumentPtrs, function ($argumentPtr) use ($tokens) {
9✔
476
                                return isset(Tokens::$emptyTokens[$tokens[$argumentPtr]['code']]) === false;
12✔
477
                        }));
12✔
478
                        if (empty($argumentPtrs)) {
12✔
479
                                continue;
×
480
                        }
481
                        if (!isset($tokens[$argumentPtrs[0]])) {
12✔
482
                                continue;
×
483
                        }
484
                        $argumentFirstToken = $tokens[$argumentPtrs[0]];
12✔
485
                        if ($argumentFirstToken['code'] === T_ARRAY) {
12✔
486
                                // It's an array argument, recurse.
487
                                $arrayArguments = self::findFunctionCallArguments($phpcsFile, $argumentPtrs[0]);
12✔
488
                                $variablePositionsAndNames = array_merge($variablePositionsAndNames, self::getVariablesInsideCompact($phpcsFile, $stackPtr, $arrayArguments));
12✔
489
                                continue;
12✔
490
                        }
491
                        if (count($argumentPtrs) > 1) {
12✔
492
                                // Complex argument, we can't handle it, ignore.
493
                                continue;
12✔
494
                        }
495
                        if ($argumentFirstToken['code'] === T_CONSTANT_ENCAPSED_STRING) {
12✔
496
                                // Single-quoted string literal, ie compact('whatever').
497
                                // Substr is to strip the enclosing single-quotes.
498
                                $varName = substr($argumentFirstToken['content'], 1, -1);
12✔
499
                                $variable = new VariableInfo($varName);
12✔
500
                                $variable->firstRead = $argumentPtrs[0];
12✔
501
                                $variablePositionsAndNames[] = $variable;
12✔
502
                                continue;
12✔
503
                        }
504
                        if ($argumentFirstToken['code'] === T_DOUBLE_QUOTED_STRING) {
12✔
505
                                // Double-quoted string literal.
506
                                $regexp = Constants::getDoubleQuotedVarRegexp();
12✔
507
                                if (! empty($regexp) && preg_match($regexp, $argumentFirstToken['content'])) {
12✔
508
                                        // Bail if the string needs variable expansion, that's runtime stuff.
509
                                        continue;
12✔
510
                                }
511
                                // Substr is to strip the enclosing double-quotes.
512
                                $varName = substr($argumentFirstToken['content'], 1, -1);
×
513
                                $variable = new VariableInfo($varName);
×
514
                                $variable->firstRead = $argumentPtrs[0];
×
515
                                $variablePositionsAndNames[] = $variable;
×
516
                                continue;
×
517
                        }
518
                }
3✔
519
                return $variablePositionsAndNames;
12✔
520
        }
521

522
        /**
523
         * Return the token index of the scope start for a token
524
         *
525
         * For a variable within a function body, or a variable within a function
526
         * definition argument list, this will return the function keyword's index.
527
         *
528
         * For a variable within a "use" import list within a function definition,
529
         * this will return the enclosing scope, not the function keyword. This is
530
         * important to note because the "use" keyword performs double-duty, defining
531
         * variables for the function's scope, and consuming the variables in the
532
         * enclosing scope. Use `getUseIndexForUseImport` to determine if this
533
         * token needs to be treated as a "use".
534
         *
535
         * For a variable within an arrow function definition argument list,
536
         * this will return the arrow function's keyword index.
537
         *
538
         * For a variable in an arrow function body, this will return the enclosing
539
         * function's index, which may be incorrect.
540
         *
541
         * Since a variable in an arrow function's body may be imported from the
542
         * enclosing scope, it's important to test to see if the variable is in an
543
         * arrow function and also check its enclosing scope separately.
544
         *
545
         * @param File $phpcsFile
546
         * @param int  $stackPtr
547
         *
548
         * @return ?int
549
         */
550
        public static function findVariableScopeExceptArrowFunctions(File $phpcsFile, $stackPtr)
356✔
551
        {
552
                $tokens = $phpcsFile->getTokens();
356✔
553
                $allowedTypes = [
178✔
554
                        T_VARIABLE,
356✔
555
                        T_DOUBLE_QUOTED_STRING,
356✔
556
                        T_HEREDOC,
356✔
557
                        T_STRING,
356✔
558
                        T_NAME_FULLY_QUALIFIED,
356✔
559
                        T_NAME_QUALIFIED,
356✔
560
                        T_NAME_RELATIVE,
356✔
561
                ];
267✔
562
                if (! in_array($tokens[$stackPtr]['code'], $allowedTypes, true)) {
356✔
563
                        throw new \Exception("Cannot find variable scope for non-variable {$tokens[$stackPtr]['type']}");
×
564
                }
565

566
                $startOfTokenScope = self::getStartOfTokenScope($phpcsFile, $stackPtr);
356✔
567
                if (is_int($startOfTokenScope) && $startOfTokenScope > 0) {
356✔
568
                        return $startOfTokenScope;
336✔
569
                }
570

571
                // If there is no "conditions" array, this is a function definition argument.
572
                if (self::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
292✔
573
                        $functionPtr = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
232✔
574
                        if (! is_int($functionPtr)) {
232✔
575
                                throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
576
                        }
577
                        return $functionPtr;
232✔
578
                }
579

580
                self::debug('Cannot find function scope for variable at', $stackPtr);
124✔
581
                return $startOfTokenScope;
124✔
582
        }
583

584
        /**
585
         * Return the token index of the scope start for a variable token
586
         *
587
         * This will only work for a variable within a function's body. Otherwise,
588
         * see `findVariableScope`, which is more complex.
589
         *
590
         * Note that if used on a variable in an arrow function, it will return the
591
         * enclosing function's scope, which may be incorrect.
592
         *
593
         * @param File $phpcsFile
594
         * @param int  $stackPtr
595
         *
596
         * @return ?int
597
         */
598
        private static function getStartOfTokenScope(File $phpcsFile, $stackPtr)
356✔
599
        {
600
                $tokens = $phpcsFile->getTokens();
356✔
601
                $token = $tokens[$stackPtr];
356✔
602

603
                $inClass = false;
356✔
604
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
356✔
605
                $functionTokenTypes = [
178✔
606
                        T_FUNCTION,
356✔
607
                        T_CLOSURE,
356✔
608
                ];
267✔
609
                foreach (array_reverse($conditions, true) as $scopePtr => $scopeCode) {
356✔
610
                        if (in_array($scopeCode, $functionTokenTypes, true) || self::isArrowFunction($phpcsFile, $scopePtr)) {
340✔
611
                                return $scopePtr;
336✔
612
                        }
613
                        if (isset(Tokens::$ooScopeTokens[$scopeCode]) === true) {
164✔
614
                                $inClass = true;
68✔
615
                        }
17✔
616
                }
82✔
617

618
                if ($inClass) {
292✔
619
                        // If this is inside a class and not inside a function, this is either a
620
                        // class member variable definition, or a function argument. If it is a
621
                        // variable definition, it has no scope on its own (it can only be used
622
                        // with an object reference). If it is a function argument, we need to do
623
                        // more work (see `findVariableScopeExceptArrowFunctions`).
624
                        return null;
60✔
625
                }
626

627
                // If we can't find a scope, let's use the first token of the file.
628
                return 0;
252✔
629
        }
630

631
        /**
632
         * @param File $phpcsFile
633
         * @param int  $stackPtr
634
         * @param int  $enclosingScopeIndex
635
         *
636
         * @return ?int
637
         */
638
        public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
352✔
639
        {
640
                $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
352✔
641
                if (! is_int($arrowFunctionIndex)) {
352✔
642
                        return null;
352✔
643
                }
644
                $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex);
36✔
645
                if (! $arrowFunctionInfo) {
36✔
646
                        return null;
×
647
                }
648

649
                // We found the closest arrow function before this token. If the token is
650
                // within the scope of that arrow function, then return it.
651
                if ($stackPtr >= $arrowFunctionInfo['scope_opener'] && $stackPtr <= $arrowFunctionInfo['scope_closer']) {
36✔
652
                        return $arrowFunctionIndex;
36✔
653
                }
654

655
                // If the token is after the scope of the closest arrow function, we may
656
                // still be inside the scope of a nested arrow function, so we need to
657
                // search further back until we are certain there are no more arrow
658
                // functions.
659
                if ($stackPtr > $arrowFunctionInfo['scope_closer']) {
36✔
660
                        return self::getContainingArrowFunctionIndex($phpcsFile, $arrowFunctionIndex, $enclosingScopeIndex);
36✔
661
                }
662

663
                return null;
36✔
664
        }
665

666
        /**
667
         * Move back from the stackPtr to the start of the enclosing scope until we
668
         * find a 'fn' token that starts an arrow function, returning the index of
669
         * that token. Returns null if there are no arrow functions before stackPtr.
670
         *
671
         * Note that this does not guarantee that stackPtr is inside the arrow
672
         * function scope we find!
673
         *
674
         * @param File $phpcsFile
675
         * @param int  $stackPtr
676
         * @param int  $enclosingScopeIndex
677
         *
678
         * @return ?int
679
         */
680
        private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
352✔
681
        {
682
                $tokens = $phpcsFile->getTokens();
352✔
683
                for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) {
352✔
684
                        $token = $tokens[$index];
352✔
685
                        if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) {
352✔
686
                                return $index;
36✔
687
                        }
688
                }
88✔
689
                return null;
352✔
690
        }
691

692
        /**
693
         * @param File $phpcsFile
694
         * @param int  $stackPtr
695
         *
696
         * @return bool
697
         */
698
        public static function isArrowFunction(File $phpcsFile, $stackPtr)
356✔
699
        {
700
                $tokens = $phpcsFile->getTokens();
356✔
701
                return $tokens[$stackPtr]['code'] === T_FN;
356✔
702
        }
703

704
        /**
705
         * Find the opening and closing scope positions for an arrow function if the
706
         * given position is the start of the arrow function (the `fn` keyword
707
         * token).
708
         *
709
         * Returns null if the passed token is not an arrow function keyword.
710
         *
711
         * If the token is an arrow function keyword, the scope opener is returned as
712
         * the provided position.
713
         *
714
         * @param File $phpcsFile
715
         * @param int  $stackPtr
716
         *
717
         * @return ?array<string, int>
718
         */
719
        public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
36✔
720
        {
721
                $tokens = $phpcsFile->getTokens();
36✔
722

723
                if ($tokens[$stackPtr]['code'] !== T_FN) {
36✔
NEW
724
                        return null;
×
725
                }
726

727
                if (!isset($tokens[$stackPtr]['scope_closer'])) {
36✔
728
                        return null;
×
729
                }
730

731
                return [
18✔
732
                        'scope_opener' => $tokens[$stackPtr]['scope_opener'],
36✔
733
                        'scope_closer' => $tokens[$stackPtr]['scope_closer'],
36✔
734
                ];
27✔
735
        }
736

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

754
                // Use PHPCSUtils to get detailed assignment information
755
                try {
756
                        $assignments = \PHPCSUtils\Utils\Lists::getAssignments($phpcsFile, $listOpenerIndex);
48✔
757
                } catch (\PHPCSUtils\Exceptions\UnexpectedTokenType $e) {
42✔
758
                        // Not a list token
759
                        return null;
40✔
760
                }
761

762
                if (empty($assignments)) {
32✔
NEW
763
                        return null;
×
764
                }
765

766
                // Extract just the variable token positions for backward compatibility
767
                $variablePtrs = [];
32✔
768
                foreach ($assignments as $assignment) {
32✔
769
                        // Skip empty list items like in: list($a, , $b)
770
                        if ($assignment['is_empty']) {
32✔
771
                                continue;
4✔
772
                        }
773

774
                        // For nested lists, recursively get the assignments
775
                        if ($assignment['is_nested_list'] && $assignment['assignment_token'] !== false) {
32✔
776
                                $nestedVars = self::getListAssignments($phpcsFile, $assignment['assignment_token']);
4✔
777
                                if (is_array($nestedVars)) {
4✔
778
                                        $variablePtrs = array_merge($variablePtrs, $nestedVars);
4✔
779
                                }
1✔
780
                                continue;
4✔
781
                        }
782

783
                        // For regular variables, use the assignment_token which points to the T_VARIABLE
784
                        if ($assignment['assignment_token'] !== false && $assignment['variable'] !== false) {
32✔
785
                                $variablePtrs[] = $assignment['assignment_token'];
32✔
786
                        }
8✔
787
                }
8✔
788

789
                return empty($variablePtrs) ? null : $variablePtrs;
32✔
790
        }
791

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

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

838
        /**
839
         * @param string $pattern
840
         * @param string $value
841
         *
842
         * @return string[]
843
         */
844
        public static function splitStringToArray($pattern, $value)
28✔
845
        {
846
                if (empty($pattern)) {
28✔
847
                        return [];
×
848
                }
849
                $result = preg_split($pattern, $value);
28✔
850
                return is_array($result) ? $result : [];
28✔
851
        }
852

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

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

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

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

924
                return false;
332✔
925
        }
926

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

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

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

955
                return $blockIndices;
24✔
956
        }
957

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

970
        /**
971
         * @param File $phpcsFile
972
         * @param int  $scopeStartIndex
973
         *
974
         * @return int
975
         */
976
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
356✔
977
        {
978
                $tokens = $phpcsFile->getTokens();
356✔
979
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
356✔
980
                if ($scopeStartIndex === 0) {
356✔
981
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
356✔
982
                }
89✔
983
                return $scopeCloserIndex;
356✔
984
        }
985

986
        /**
987
         * @param File $phpcsFile
988
         *
989
         * @return int
990
         */
991
        public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
356✔
992
        {
993
                $tokens = $phpcsFile->getTokens();
356✔
994
                foreach (array_reverse($tokens, true) as $index => $token) {
356✔
995
                        if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
356✔
996
                                return $index;
356✔
997
                        }
998
                }
87✔
999
                self::debug('no non-empty token found for end of file');
×
1000
                return 0;
×
1001
        }
1002

1003
        /**
1004
         * @param VariableInfo $varInfo
1005
         * @param ScopeInfo    $scopeInfo
1006
         *
1007
         * @return bool
1008
         */
1009
        public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo)
68✔
1010
        {
1011
                $foundVarPosition = false;
68✔
1012
                foreach ($scopeInfo->variables as $variable) {
68✔
1013
                        if ($variable === $varInfo) {
68✔
1014
                                $foundVarPosition = true;
68✔
1015
                                continue;
68✔
1016
                        }
1017
                        if (! $foundVarPosition) {
44✔
1018
                                continue;
36✔
1019
                        }
1020
                        if ($variable->scopeType !== ScopeType::PARAM) {
44✔
1021
                                continue;
36✔
1022
                        }
1023
                        if ($variable->firstRead) {
16✔
1024
                                return true;
16✔
1025
                        }
1026
                }
17✔
1027
                return false;
68✔
1028
        }
1029

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

1061
        /**
1062
         * Find the index of the function keyword for a token in a function call's arguments
1063
         *
1064
         * For the variable `$foo` in the expression `doSomething($foo)`, this will
1065
         * return the index of the `doSomething` token.
1066
         *
1067
         * @param File $phpcsFile
1068
         * @param int  $stackPtr
1069
         *
1070
         * @return ?int
1071
         */
1072
        public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr)
324✔
1073
        {
1074
                $tokens = $phpcsFile->getTokens();
324✔
1075
                $token = $tokens[$stackPtr];
324✔
1076
                if (empty($token['nested_parenthesis'])) {
324✔
1077
                        return null;
316✔
1078
                }
1079
                /**
1080
                 * @var list<int|string>
1081
                 */
1082
                $startingParenthesis = array_keys($token['nested_parenthesis']);
80✔
1083
                $startOfArguments = end($startingParenthesis);
80✔
1084
                if (! is_int($startOfArguments)) {
80✔
1085
                        return null;
×
1086
                }
1087

1088
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
80✔
1089
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
80✔
1090
                if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
80✔
1091
                        return null;
×
1092
                }
1093
                if (
1094
                        $tokens[$functionPtr]['content'] === 'function'
80✔
1095
                        || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))
80✔
1096
                ) {
20✔
1097
                        // If there is a function/fn keyword before the beginning of the parens,
1098
                        // this is a function definition and not a function call.
1099
                        return null;
×
1100
                }
1101
                if (! empty($tokens[$functionPtr]['scope_opener'])) {
80✔
1102
                        // If the alleged function name has a scope, this is not a function call.
1103
                        return null;
28✔
1104
                }
1105

1106
                $functionNameType = $tokens[$functionPtr]['code'];
72✔
1107
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
72✔
1108
                        // If the alleged function name is not a variable or a string, this is
1109
                        // not a function call.
1110
                        return null;
32✔
1111
                }
1112

1113
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
56✔
1114
                        // If the variable is inside a different scope than the function name,
1115
                        // the function call doesn't apply to the variable.
1116
                        return null;
32✔
1117
                }
1118

1119
                return $functionPtr;
24✔
1120
        }
1121

1122
        /**
1123
         * @param File $phpcsFile
1124
         * @param int  $stackPtr
1125
         *
1126
         * @return bool
1127
         */
1128
        public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
332✔
1129
        {
1130
                // Use PHPCSUtils which handles all edge cases across PHP/PHPCS versions
1131
                return Context::inIsset($phpcsFile, $stackPtr) || Context::inEmpty($phpcsFile, $stackPtr);
332✔
1132
        }
1133

1134
        /**
1135
         * @param File $phpcsFile
1136
         * @param int  $stackPtr
1137
         *
1138
         * @return bool
1139
         */
1140
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
260✔
1141
        {
1142
                $tokens = $phpcsFile->getTokens();
260✔
1143
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
260✔
1144

1145
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
260✔
1146
                if (! is_int($arrayPushOperatorIndex1)) {
260✔
1147
                        return false;
×
1148
                }
1149
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
260✔
1150
                        return false;
260✔
1151
                }
1152

1153
                $arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true));
36✔
1154
                if (! is_int($arrayPushOperatorIndex2)) {
36✔
1155
                        return false;
×
1156
                }
1157
                if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') {
36✔
1158
                        return false;
8✔
1159
                }
1160

1161
                $arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true));
28✔
1162
                if (! is_int($arrayPushOperatorIndex3)) {
28✔
1163
                        return false;
×
1164
                }
1165
                if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') {
28✔
1166
                        return false;
×
1167
                }
1168

1169
                return true;
28✔
1170
        }
1171

1172
        /**
1173
         * @param File $phpcsFile
1174
         * @param int  $stackPtr
1175
         *
1176
         * @return bool
1177
         */
1178
        public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
260✔
1179
        {
1180
                // Use PHPCSUtils which handles all edge cases across PHP/PHPCS versions
1181
                return Context::inUnset($phpcsFile, $stackPtr);
260✔
1182
        }
1183

1184
        /**
1185
         * @param File $phpcsFile
1186
         * @param int  $stackPtr
1187
         *
1188
         * @return bool
1189
         */
1190
        public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr)
324✔
1191
        {
1192
                $previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
324✔
1193
                if (! is_int($previousStatementPtr)) {
324✔
1194
                        $previousStatementPtr = 1;
40✔
1195
                }
10✔
1196
                $previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
324✔
1197
                if (is_int($previousTokenPtr)) {
324✔
1198
                        return true;
4✔
1199
                }
1200
                return false;
324✔
1201
        }
1202

1203
        /**
1204
         * @param File $phpcsFile
1205
         * @param int  $stackPtr
1206
         *
1207
         * @return bool
1208
         */
1209
        public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
344✔
1210
        {
1211
                // Is the next non-whitespace an assignment?
1212
                $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
344✔
1213
                if (! is_int($assignPtr)) {
344✔
1214
                        return false;
336✔
1215
                }
1216

1217
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1218
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
324✔
1219
                        self::debug('found variable variable');
4✔
1220
                        return false;
4✔
1221
                }
1222
                return true;
324✔
1223
        }
1224

1225
        /**
1226
         * @param File $phpcsFile
1227
         * @param int  $stackPtr
1228
         *
1229
         * @return bool
1230
         */
1231
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
324✔
1232
        {
1233
                $tokens = $phpcsFile->getTokens();
324✔
1234

1235
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
324✔
1236
                if ($prev === false) {
324✔
1237
                        return false;
×
1238
                }
1239
                if ($tokens[$prev]['code'] === T_DOLLAR) {
324✔
1240
                        return true;
4✔
1241
                }
1242
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
324✔
1243
                        return false;
268✔
1244
                }
1245

1246
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
232✔
1247
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
232✔
1248
                        return true;
×
1249
                }
1250
                return false;
232✔
1251
        }
1252

1253
        /**
1254
         * @param File $phpcsFile
1255
         * @param int  $stackPtr
1256
         *
1257
         * @return EnumInfo|null
1258
         */
1259
        public static function makeEnumInfo(File $phpcsFile, $stackPtr)
4✔
1260
        {
1261
                $tokens = $phpcsFile->getTokens();
4✔
1262
                $token = $tokens[$stackPtr];
4✔
1263

1264
                if (isset($token['scope_opener'])) {
4✔
1265
                        $blockStart = $token['scope_opener'];
4✔
1266
                        $blockEnd = $token['scope_closer'];
4✔
1267
                } else {
1✔
1268
                        // Enums before phpcs could detect them do not have scopes so we have to
1269
                        // find them ourselves.
1270

1271
                        $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
4✔
1272
                        if (! is_int($blockStart)) {
4✔
1273
                                return null;
4✔
1274
                        }
UNCOV
1275
                        $blockEnd = $tokens[$blockStart]['bracket_closer'];
×
1276
                }
1277

1278
                return new EnumInfo(
4✔
1279
                        $stackPtr,
4✔
1280
                        $blockStart,
4✔
1281
                        $blockEnd
3✔
1282
                );
3✔
1283
        }
1284

1285
        /**
1286
         * @param File $phpcsFile
1287
         * @param int  $stackPtr
1288
         *
1289
         * @return ForLoopInfo
1290
         */
1291
        public static function makeForLoopInfo(File $phpcsFile, $stackPtr)
8✔
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 {
2✔
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
                }
2✔
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
6✔
1365
                );
6✔
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)
352✔
1374
        {
1375
                foreach ($forLoops as $forLoop) {
352✔
1376
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1377
                                return $forLoop;
8✔
1378
                        }
1379
                }
88✔
1380
                return null;
352✔
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)
232✔
1394
        {
1395
                // If we are not in a function's parameters, this is not promotion.
1396
                $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
232✔
1397
                if (! $functionIndex) {
232✔
1398
                        return false;
×
1399
                }
1400

1401
                $tokens = $phpcsFile->getTokens();
232✔
1402

1403
                // Move backwards from the token, ignoring whitespace, typehints, and the
1404
                // 'readonly' keyword, and return true if the previous token is a
1405
                // visibility keyword (eg: `public`).
1406
                for ($i = $stackPtr - 1; $i > $functionIndex; $i--) {
232✔
1407
                        if (in_array($tokens[$i]['code'], Tokens::$scopeModifiers, true)) {
232✔
1408
                                return true;
12✔
1409
                        }
1410
                        if (in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) {
232✔
1411
                                continue;
160✔
1412
                        }
1413
                        if ($tokens[$i]['content'] === 'readonly') {
232✔
1414
                                continue;
8✔
1415
                        }
1416
                        if (self::isTokenPartOfTypehint($phpcsFile, $i)) {
232✔
1417
                                continue;
32✔
1418
                        }
1419
                        return false;
228✔
1420
                }
1421
                return false;
×
1422
        }
1423

1424
        /**
1425
         * If looking at a function call token, return a string for the full function
1426
         * name including any inline namespace.
1427
         *
1428
         * So for example, if the call looks like `\My\Namespace\doSomething($bar)`
1429
         * and `$stackPtr` refers to `doSomething`, this will return
1430
         * `\My\Namespace\doSomething`.
1431
         *
1432
         * @param File $phpcsFile
1433
         * @param int  $stackPtr
1434
         *
1435
         * @return string|null
1436
         */
1437
        public static function getFunctionNameWithNamespace(File $phpcsFile, $stackPtr)
156✔
1438
        {
1439
                $tokens = $phpcsFile->getTokens();
156✔
1440

1441
                if (! isset($tokens[$stackPtr])) {
156✔
1442
                        return null;
×
1443
                }
1444
                $startOfScope = self::findVariableScope($phpcsFile, $stackPtr);
156✔
1445
                $functionName = $tokens[$stackPtr]['content'];
156✔
1446

1447
                // In PHPCS 4.x, T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, and T_NAME_RELATIVE
1448
                // tokens already contain the full namespaced name, so we can return early.
1449
                if ($tokens[$stackPtr]['code'] === T_NAME_FULLY_QUALIFIED) {
156✔
1450
                        return $functionName;
8✔
1451
                }
1452
                if ($tokens[$stackPtr]['code'] === T_NAME_QUALIFIED) {
156✔
1453
                        return $functionName;
×
1454
                }
1455
                if ($tokens[$stackPtr]['code'] === T_NAME_RELATIVE) {
156✔
1456
                        return $functionName;
×
1457
                }
1458

1459
                // Move backwards from the token, collecting namespace separators and
1460
                // strings, until we encounter whitespace or something else.
1461
                $partOfNamespace = [
78✔
1462
                        T_NS_SEPARATOR,
156✔
1463
                        T_STRING,
156✔
1464
                        T_NAME_QUALIFIED,
156✔
1465
                        T_NAME_RELATIVE,
156✔
1466
                        T_NAME_FULLY_QUALIFIED,
156✔
1467
                ];
117✔
1468
                for ($i = $stackPtr - 1; $i > $startOfScope; $i--) {
156✔
1469
                        if (! in_array($tokens[$i]['code'], $partOfNamespace, true)) {
156✔
1470
                                break;
156✔
1471
                        }
1472
                        $functionName = "{$tokens[$i]['content']}{$functionName}";
8✔
1473
                }
4✔
1474
                return $functionName;
156✔
1475
        }
1476

1477
        /**
1478
         * Return false if the token is definitely not part of a typehint
1479
         *
1480
         * @param File $phpcsFile
1481
         * @param int  $stackPtr
1482
         *
1483
         * @return bool
1484
         */
1485
        private static function isTokenPossiblyPartOfTypehint(File $phpcsFile, $stackPtr)
232✔
1486
        {
1487
                $tokens = $phpcsFile->getTokens();
232✔
1488
                $token = $tokens[$stackPtr];
232✔
1489
                if ($token['code'] === 'PHPCS_T_NULLABLE') {
232✔
1490
                        return true;
8✔
1491
                }
1492
                if ($token['code'] === T_NAME_QUALIFIED) {
232✔
1493
                        return true;
×
1494
                }
1495
                if ($token['code'] === T_NAME_RELATIVE) {
232✔
1496
                        return true;
×
1497
                }
1498
                if ($token['code'] === T_NAME_FULLY_QUALIFIED) {
232✔
1499
                        return true;
12✔
1500
                }
1501
                if ($token['code'] === T_NS_SEPARATOR) {
232✔
1502
                        return true;
12✔
1503
                }
1504
                if ($token['code'] === T_STRING) {
232✔
1505
                        return true;
32✔
1506
                }
1507
                if ($token['code'] === T_TRUE) {
232✔
1508
                        return true;
8✔
1509
                }
1510
                if ($token['code'] === T_FALSE) {
232✔
1511
                        return true;
8✔
1512
                }
1513
                if ($token['code'] === T_NULL) {
232✔
1514
                        return true;
8✔
1515
                }
1516
                if ($token['content'] === '|') {
232✔
1517
                        return true;
16✔
1518
                }
1519
                if (in_array($token['code'], Tokens::$emptyTokens)) {
232✔
1520
                        return true;
32✔
1521
                }
1522
                return false;
232✔
1523
        }
1524

1525
        /**
1526
         * Return true if the token is inside a typehint
1527
         *
1528
         * @param File $phpcsFile
1529
         * @param int  $stackPtr
1530
         *
1531
         * @return bool
1532
         */
1533
        public static function isTokenPartOfTypehint(File $phpcsFile, $stackPtr)
232✔
1534
        {
1535
                $tokens = $phpcsFile->getTokens();
232✔
1536

1537
                if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $stackPtr)) {
232✔
1538
                        return false;
228✔
1539
                }
1540

1541
                // Examine every following token, ignoring everything that might be part of
1542
                // a typehint. If we find a variable at the end, this is part of a
1543
                // typehint.
1544
                $i = $stackPtr;
32✔
1545
                while (true) {
32✔
1546
                        $i += 1;
32✔
1547
                        if (! isset($tokens[$i])) {
32✔
1548
                                return false;
×
1549
                        }
1550
                        if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $i)) {
32✔
1551
                                return ($tokens[$i]['code'] === T_VARIABLE);
32✔
1552
                        }
1553
                }
8✔
1554
        }
1555

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

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