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

sirbrillig / phpcs-variable-analysis / 24632078366

19 Apr 2026 02:39PM UTC coverage: 94.489% (+0.8%) from 93.732%
24632078366

Pull #360

github

sirbrillig
Simplify getUseIndexForUseImport using findContainingOpeningBracket()

Replaces the manual findPrevious call with an exclusion list with the
existing findContainingOpeningBracket() abstraction, and simplifies the
second findPrevious to skip only empty tokens.
Pull Request #360: Migrate to PHPCSUtils

67 of 70 new or added lines in 2 files covered. (95.71%)

8 existing lines in 2 files now uncovered.

1749 of 1851 relevant lines covered (94.49%)

138.7 hits per line

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

91.69
/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\Conditions;
14
use PHPCSUtils\Utils\Context;
15
use PHPCSUtils\Utils\FunctionDeclarations;
16
use PHPCSUtils\Utils\Lists;
17
use PHPCSUtils\Utils\Parentheses;
18
use PHPCSUtils\Utils\PassedParameters;
19

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

36
        /**
37
         * @param int|bool $value
38
         *
39
         * @return ?int
40
         */
41
        public static function getIntOrNull($value)
344✔
42
        {
43
                return is_int($value) ? $value : null;
344✔
44
        }
45

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

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

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

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

106
        /**
107
         * @param File $phpcsFile
108
         * @param int  $stackPtr
109
         *
110
         * @return bool
111
         */
112
        public static function areAnyConditionsAClass(File $phpcsFile, $stackPtr)
40✔
113
        {
114
                $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
40✔
115
                if (defined('T_ENUM')) {
40✔
116
                        $classlikeCodes[] = T_ENUM;
40✔
117
                }
10✔
118
                $classlikeCodes[] = 'PHPCS_T_ENUM';
40✔
119
                return Conditions::hasCondition($phpcsFile, $stackPtr, $classlikeCodes);
40✔
120
        }
121

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

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

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

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

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

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

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

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

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

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

284
                $openParenPtr = self::findContainingOpeningBracket($phpcsFile, $stackPtr);
352✔
285
                if (! is_int($openParenPtr)) {
352✔
286
                        return null;
348✔
287
                }
288

289
                $usePtr = self::getIntOrNull(
264✔
290
                        $phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true, null, true)
264✔
291
                );
198✔
292
                if (! is_int($usePtr) || $tokens[$usePtr]['code'] !== T_USE) {
264✔
293
                        return null;
260✔
294
                }
295
                return $usePtr;
24✔
296
        }
297

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

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

333
        /**
334
         * @param File $phpcsFile
335
         * @param int  $stackPtr
336
         *
337
         * @return array<int|string, array<string, int|string>>
338
         */
339
        public static function findFunctionCallArguments(File $phpcsFile, $stackPtr)
36✔
340
        {
341
                $tokens = $phpcsFile->getTokens();
36✔
342

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

352
                return PassedParameters::getParameters($phpcsFile, $stackPtr);
36✔
353
        }
354

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

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

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

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

402
                $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
356✔
403

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

421
                return $enclosingScopeIndex;
356✔
422
        }
423

424
        /**
425
         * Return the variable names and positions of each variable targetted by a `compact()` call.
426
         *
427
         * @param File                                         $phpcsFile
428
         * @param int                                          $stackPtr
429
         * @param array<int|string, array<string, int|string>> $arguments The parameters from PassedParameters::getParameters()
430
         *
431
         * @return array<VariableInfo> each variable's firstRead position and its name; other VariableInfo properties are not set!
432
         */
433
        public static function getVariablesInsideCompact(File $phpcsFile, $stackPtr, $arguments)
13✔
434
        {
435
                $tokens = $phpcsFile->getTokens();
13✔
436
                $variablePositionsAndNames = [];
12✔
437

438
                foreach ($arguments as $param) {
12✔
439
                        // Find the first non-empty token in this argument's range.
440
                        $firstNonEmpty = null;
12✔
441
                        $nonEmptyCount = 0;
12✔
442
                        for ($i = (int)$param['start']; $i <= (int)$param['end']; $i++) {
12✔
443
                                if (!isset(Tokens::$emptyTokens[$tokens[$i]['code']])) {
12✔
444
                                        if ($firstNonEmpty === null) {
12✔
445
                                                $firstNonEmpty = $i;
12✔
446
                                        }
3✔
447
                                        $nonEmptyCount++;
12✔
448
                                }
3✔
449
                        }
3✔
450

451
                        if ($firstNonEmpty === null) {
12✔
UNCOV
452
                                continue;
×
453
                        }
454

455
                        $argumentFirstToken = $tokens[$firstNonEmpty];
12✔
456

457
                        if ($argumentFirstToken['code'] === T_ARRAY) {
12✔
458
                                // It's an array argument, recurse.
459
                                $arrayArguments = PassedParameters::getParameters($phpcsFile, $firstNonEmpty);
12✔
460
                                $variablePositionsAndNames = array_merge(
12✔
461
                                        $variablePositionsAndNames,
12✔
462
                                        self::getVariablesInsideCompact($phpcsFile, $stackPtr, $arrayArguments)
12✔
463
                                );
9✔
464
                                continue;
12✔
465
                        }
1✔
466

467
                        if ($nonEmptyCount > 1) {
12✔
468
                                // Complex argument, we can't handle it, ignore.
469
                                continue;
12✔
470
                        }
471

472
                        if ($argumentFirstToken['code'] === T_CONSTANT_ENCAPSED_STRING) {
12✔
473
                                // Single-quoted string literal, ie compact('whatever').
474
                                // Substr is to strip the enclosing single-quotes.
475
                                $varName = substr($argumentFirstToken['content'], 1, -1);
12✔
476
                                $variable = new VariableInfo($varName);
12✔
477
                                $variable->firstRead = $firstNonEmpty;
12✔
478
                                $variablePositionsAndNames[] = $variable;
12✔
479
                                continue;
12✔
480
                        }
481

482
                        if ($argumentFirstToken['code'] === T_DOUBLE_QUOTED_STRING) {
12✔
483
                                // Double-quoted string literal.
484
                                $regexp = Constants::getDoubleQuotedVarRegexp();
12✔
485
                                if (! empty($regexp) && preg_match($regexp, $argumentFirstToken['content'])) {
12✔
486
                                        // Bail if the string needs variable expansion, that's runtime stuff.
487
                                        continue;
12✔
488
                                }
489
                                // Substr is to strip the enclosing double-quotes.
490
                                $varName = substr($argumentFirstToken['content'], 1, -1);
×
491
                                $variable = new VariableInfo($varName);
×
NEW
492
                                $variable->firstRead = $firstNonEmpty;
×
493
                                $variablePositionsAndNames[] = $variable;
×
494
                        }
495
                }
3✔
496
                return $variablePositionsAndNames;
12✔
497
        }
498

499
        /**
500
         * Return the token index of the scope start for a token
501
         *
502
         * For a variable within a function body, or a variable within a function
503
         * definition argument list, this will return the function keyword's index.
504
         *
505
         * For a variable within a "use" import list within a function definition,
506
         * this will return the enclosing scope, not the function keyword. This is
507
         * important to note because the "use" keyword performs double-duty, defining
508
         * variables for the function's scope, and consuming the variables in the
509
         * enclosing scope. Use `getUseIndexForUseImport` to determine if this
510
         * token needs to be treated as a "use".
511
         *
512
         * For a variable within an arrow function definition argument list,
513
         * this will return the arrow function's keyword index.
514
         *
515
         * For a variable in an arrow function body, this will return the enclosing
516
         * function's index, which may be incorrect.
517
         *
518
         * Since a variable in an arrow function's body may be imported from the
519
         * enclosing scope, it's important to test to see if the variable is in an
520
         * arrow function and also check its enclosing scope separately.
521
         *
522
         * @param File $phpcsFile
523
         * @param int  $stackPtr
524
         *
525
         * @return ?int
526
         */
527
        public static function findVariableScopeExceptArrowFunctions(File $phpcsFile, $stackPtr)
356✔
528
        {
529
                $tokens = $phpcsFile->getTokens();
356✔
530
                $allowedTypes = [
178✔
531
                        T_VARIABLE,
356✔
532
                        T_DOUBLE_QUOTED_STRING,
356✔
533
                        T_HEREDOC,
356✔
534
                        T_STRING,
356✔
535
                        T_NAME_FULLY_QUALIFIED,
356✔
536
                        T_NAME_QUALIFIED,
356✔
537
                        T_NAME_RELATIVE,
356✔
538
                ];
267✔
539
                if (! in_array($tokens[$stackPtr]['code'], $allowedTypes, true)) {
356✔
540
                        throw new \Exception("Cannot find variable scope for non-variable {$tokens[$stackPtr]['type']}");
×
541
                }
542

543
                $startOfTokenScope = self::getStartOfTokenScope($phpcsFile, $stackPtr);
356✔
544
                if (is_int($startOfTokenScope) && $startOfTokenScope > 0) {
356✔
545
                        return $startOfTokenScope;
336✔
546
                }
547

548
                // If there is no "conditions" array, this is a function definition argument.
549
                if (self::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
292✔
550
                        $functionPtr = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
232✔
551
                        if (! is_int($functionPtr)) {
232✔
552
                                throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
553
                        }
554
                        return $functionPtr;
232✔
555
                }
556

557
                self::debug('Cannot find function scope for variable at', $stackPtr);
124✔
558
                return $startOfTokenScope;
124✔
559
        }
560

561
        /**
562
         * Return the token index of the scope start for a variable token
563
         *
564
         * This will only work for a variable within a function's body. Otherwise,
565
         * see `findVariableScope`, which is more complex.
566
         *
567
         * Note that if used on a variable in an arrow function, it will return the
568
         * enclosing function's scope, which may be incorrect.
569
         *
570
         * @param File $phpcsFile
571
         * @param int  $stackPtr
572
         *
573
         * @return ?int
574
         */
575
        private static function getStartOfTokenScope(File $phpcsFile, $stackPtr)
356✔
576
        {
577
                $tokens = $phpcsFile->getTokens();
356✔
578
                $token = $tokens[$stackPtr];
356✔
579

580
                $inClass = false;
356✔
581
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
356✔
582
                $functionTokenTypes = [
178✔
583
                        T_FUNCTION,
356✔
584
                        T_CLOSURE,
356✔
585
                ];
267✔
586
                foreach (array_reverse($conditions, true) as $scopePtr => $scopeCode) {
356✔
587
                        if (in_array($scopeCode, $functionTokenTypes, true) || self::isArrowFunction($phpcsFile, $scopePtr)) {
340✔
588
                                return $scopePtr;
336✔
589
                        }
590
                        if (isset(Tokens::$ooScopeTokens[$scopeCode]) === true) {
164✔
591
                                $inClass = true;
68✔
592
                        }
17✔
593
                }
82✔
594

595
                if ($inClass) {
292✔
596
                        // If this is inside a class and not inside a function, this is either a
597
                        // class member variable definition, or a function argument. If it is a
598
                        // variable definition, it has no scope on its own (it can only be used
599
                        // with an object reference). If it is a function argument, we need to do
600
                        // more work (see `findVariableScopeExceptArrowFunctions`).
601
                        return null;
60✔
602
                }
603

604
                // If we can't find a scope, let's use the first token of the file.
605
                return 0;
252✔
606
        }
607

608
        /**
609
         * @param File $phpcsFile
610
         * @param int  $stackPtr
611
         * @param int  $enclosingScopeIndex
612
         *
613
         * @return ?int
614
         */
615
        public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
352✔
616
        {
617
                $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
352✔
618
                if (! is_int($arrowFunctionIndex)) {
352✔
619
                        return null;
352✔
620
                }
621
                $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex);
36✔
622
                if (! $arrowFunctionInfo) {
36✔
623
                        return null;
×
624
                }
625

626
                // We found the closest arrow function before this token. If the token is
627
                // within the scope of that arrow function, then return it.
628
                if ($stackPtr >= $arrowFunctionInfo['scope_opener'] && $stackPtr <= $arrowFunctionInfo['scope_closer']) {
36✔
629
                        return $arrowFunctionIndex;
36✔
630
                }
631

632
                // If the token is after the scope of the closest arrow function, we may
633
                // still be inside the scope of a nested arrow function, so we need to
634
                // search further back until we are certain there are no more arrow
635
                // functions.
636
                if ($stackPtr > $arrowFunctionInfo['scope_closer']) {
36✔
637
                        return self::getContainingArrowFunctionIndex($phpcsFile, $arrowFunctionIndex, $enclosingScopeIndex);
36✔
638
                }
639

640
                return null;
36✔
641
        }
642

643
        /**
644
         * Move back from the stackPtr to the start of the enclosing scope until we
645
         * find a 'fn' token that starts an arrow function, returning the index of
646
         * that token. Returns null if there are no arrow functions before stackPtr.
647
         *
648
         * Note that this does not guarantee that stackPtr is inside the arrow
649
         * function scope we find!
650
         *
651
         * @param File $phpcsFile
652
         * @param int  $stackPtr
653
         * @param int  $enclosingScopeIndex
654
         *
655
         * @return ?int
656
         */
657
        private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
352✔
658
        {
659
                $tokens = $phpcsFile->getTokens();
352✔
660
                for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) {
352✔
661
                        $token = $tokens[$index];
352✔
662
                        if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) {
352✔
663
                                return $index;
36✔
664
                        }
665
                }
88✔
666
                return null;
352✔
667
        }
668

669
        /**
670
         * @param File $phpcsFile
671
         * @param int  $stackPtr
672
         *
673
         * @return bool
674
         */
675
        public static function isArrowFunction(File $phpcsFile, $stackPtr)
356✔
676
        {
677
                $tokens = $phpcsFile->getTokens();
356✔
678
                return $tokens[$stackPtr]['code'] === T_FN;
356✔
679
        }
680

681
        /**
682
         * Find the opening and closing scope positions for an arrow function if the
683
         * given position is the start of the arrow function (the `fn` keyword
684
         * token).
685
         *
686
         * Returns null if the passed token is not an arrow function keyword.
687
         *
688
         * If the token is an arrow function keyword, the scope opener is returned as
689
         * the provided position.
690
         *
691
         * @param File $phpcsFile
692
         * @param int  $stackPtr
693
         *
694
         * @return ?array<string, int>
695
         */
696
        public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
36✔
697
        {
698
                $tokens = $phpcsFile->getTokens();
36✔
699

700
                if ($tokens[$stackPtr]['code'] !== T_FN) {
36✔
NEW
701
                        return null;
×
702
                }
703

704
                if (!isset($tokens[$stackPtr]['scope_closer'])) {
36✔
705
                        return null;
×
706
                }
707

708
                return [
18✔
709
                        'scope_opener' => $tokens[$stackPtr]['scope_opener'],
36✔
710
                        'scope_closer' => $tokens[$stackPtr]['scope_closer'],
36✔
711
                ];
27✔
712
        }
713

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

731
                // Use PHPCSUtils to get detailed assignment information
732
                try {
733
                        $assignments = \PHPCSUtils\Utils\Lists::getAssignments($phpcsFile, $listOpenerIndex);
48✔
734
                } catch (\PHPCSUtils\Exceptions\UnexpectedTokenType $e) {
42✔
735
                        // Not a list token
736
                        return null;
40✔
737
                }
738

739
                if (empty($assignments)) {
32✔
NEW
740
                        return null;
×
741
                }
742

743
                // Extract just the variable token positions for backward compatibility
744
                $variablePtrs = [];
32✔
745
                foreach ($assignments as $assignment) {
32✔
746
                        // Skip empty list items like in: list($a, , $b)
747
                        if ($assignment['is_empty']) {
32✔
748
                                continue;
4✔
749
                        }
750

751
                        // For nested lists, recursively get the assignments
752
                        if ($assignment['is_nested_list'] && $assignment['assignment_token'] !== false) {
32✔
753
                                $nestedVars = self::getListAssignments($phpcsFile, $assignment['assignment_token']);
4✔
754
                                if (is_array($nestedVars)) {
4✔
755
                                        $variablePtrs = array_merge($variablePtrs, $nestedVars);
4✔
756
                                }
1✔
757
                                continue;
4✔
758
                        }
759

760
                        // For regular variables, use the assignment_token which points to the T_VARIABLE
761
                        if ($assignment['assignment_token'] !== false && $assignment['variable'] !== false) {
32✔
762
                                $variablePtrs[] = $assignment['assignment_token'];
32✔
763
                        }
8✔
764
                }
8✔
765

766
                return empty($variablePtrs) ? null : $variablePtrs;
32✔
767
        }
768

769
        /**
770
         * @param File $phpcsFile
771
         * @param int  $stackPtr
772
         *
773
         * @return string[]
774
         */
775
        public static function getVariablesDefinedByArrowFunction(File $phpcsFile, $stackPtr)
36✔
776
        {
777
                $tokens = $phpcsFile->getTokens();
36✔
778
                $arrowFunctionToken = $tokens[$stackPtr];
36✔
779
                $variableNames = [];
36✔
780
                self::debug('looking for variables in arrow function token', $arrowFunctionToken);
36✔
781
                for ($index = $arrowFunctionToken['parenthesis_opener']; $index < $arrowFunctionToken['parenthesis_closer']; $index++) {
36✔
782
                        $token = $tokens[$index];
36✔
783
                        if ($token['code'] === T_VARIABLE) {
36✔
784
                                $variableNames[] = self::normalizeVarName($token['content']);
36✔
785
                        }
9✔
786
                }
9✔
787
                self::debug('found these variables in arrow function token', $variableNames);
36✔
788
                return $variableNames;
36✔
789
        }
790

791
        /**
792
         * @return void
793
         */
794
        public static function debug()
356✔
795
        {
796
                $messages = func_get_args();
356✔
797
                if (! defined('PHP_CODESNIFFER_VERBOSITY')) {
356✔
798
                        return;
×
799
                }
800
                if (PHP_CODESNIFFER_VERBOSITY <= 3) {
356✔
801
                        return;
356✔
802
                }
803
                $output = PHP_EOL . 'VariableAnalysisSniff: DEBUG:';
×
804
                foreach ($messages as $message) {
×
805
                        if (is_string($message) || is_numeric($message)) {
×
806
                                $output .= ' "' . $message . '"';
×
807
                                continue;
×
808
                        }
809
                        $output .= PHP_EOL . var_export($message, true) . PHP_EOL;
×
810
                }
811
                $output .= PHP_EOL;
×
812
                echo $output;
×
813
        }
814

815
        /**
816
         * @param string $pattern
817
         * @param string $value
818
         *
819
         * @return string[]
820
         */
821
        public static function splitStringToArray($pattern, $value)
28✔
822
        {
823
                if (empty($pattern)) {
28✔
824
                        return [];
×
825
                }
826
                $result = preg_split($pattern, $value);
28✔
827
                return is_array($result) ? $result : [];
28✔
828
        }
829

830
        /**
831
         * @param string $varName
832
         *
833
         * @return bool
834
         */
835
        public static function isVariableANumericVariable($varName)
340✔
836
        {
837
                return is_numeric(substr($varName, 0, 1));
340✔
838
        }
839

840
        /**
841
         * @param File $phpcsFile
842
         * @param int  $stackPtr
843
         *
844
         * @return bool
845
         */
846
        public static function isVariableInsideElseCondition(File $phpcsFile, $stackPtr)
332✔
847
        {
848
                $tokens = $phpcsFile->getTokens();
332✔
849
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
332✔
850
                $nonFunctionTokenTypes[] = T_OPEN_PARENTHESIS;
332✔
851
                $nonFunctionTokenTypes[] = T_INLINE_HTML;
332✔
852
                $nonFunctionTokenTypes[] = T_CLOSE_TAG;
332✔
853
                $nonFunctionTokenTypes[] = T_VARIABLE;
332✔
854
                $nonFunctionTokenTypes[] = T_ELLIPSIS;
332✔
855
                $nonFunctionTokenTypes[] = T_COMMA;
332✔
856
                $nonFunctionTokenTypes[] = T_STRING;
332✔
857
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
332✔
858
                $elsePtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $stackPtr - 1, null, true, null, true));
332✔
859
                $elseTokenTypes = [
166✔
860
                        T_ELSE,
332✔
861
                        T_ELSEIF,
332✔
862
                ];
249✔
863
                if (is_int($elsePtr) && in_array($tokens[$elsePtr]['code'], $elseTokenTypes, true)) {
332✔
864
                        return true;
16✔
865
                }
866
                return false;
332✔
867
        }
868

869
        /**
870
         * @param File $phpcsFile
871
         * @param int  $stackPtr
872
         *
873
         * @return bool
874
         */
875
        public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr)
332✔
876
        {
877
                $tokens = $phpcsFile->getTokens();
332✔
878
                $token = $tokens[$stackPtr];
332✔
879
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
332✔
880
                $elseTokenTypes = [
166✔
881
                        T_ELSE,
332✔
882
                        T_ELSEIF,
332✔
883
                ];
249✔
884
                foreach (array_reverse($conditions, true) as $scopeCode) {
332✔
885
                        if (in_array($scopeCode, $elseTokenTypes, true)) {
312✔
886
                                return true;
16✔
887
                        }
888
                }
83✔
889

890
                // Some else body code will not have conditions because it is inline (no
891
                // curly braces) so we have to look in other ways.
892
                $previousSemicolonPtr = $phpcsFile->findPrevious([T_SEMICOLON], $stackPtr - 1);
332✔
893
                if (! is_int($previousSemicolonPtr)) {
332✔
894
                        $previousSemicolonPtr = 0;
152✔
895
                }
38✔
896
                $elsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1, $previousSemicolonPtr);
332✔
897
                if (is_int($elsePtr)) {
332✔
898
                        return true;
8✔
899
                }
900

901
                return false;
332✔
902
        }
903

904
        /**
905
         * @param File $phpcsFile
906
         * @param int  $stackPtr
907
         *
908
         * @return int[]
909
         */
910
        public static function getAttachedBlockIndicesForElse(File $phpcsFile, $stackPtr)
24✔
911
        {
912
                $currentElsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1);
24✔
913
                if (! is_int($currentElsePtr)) {
24✔
914
                        throw new \Exception("Cannot find expected else at {$stackPtr}");
×
915
                }
916

917
                $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
24✔
918
                if (! is_int($ifPtr)) {
24✔
919
                        throw new \Exception("Cannot find if for else at {$stackPtr}");
×
920
                }
921
                $blockIndices = [$ifPtr];
24✔
922

923
                $previousElseIfPtr = $currentElsePtr;
24✔
924
                do {
925
                        $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
24✔
926
                        if (is_int($elseIfPtr)) {
24✔
927
                                $blockIndices[] = $elseIfPtr;
16✔
928
                                $previousElseIfPtr = $elseIfPtr;
16✔
929
                        }
4✔
930
                } while (is_int($elseIfPtr));
24✔
931

932
                return $blockIndices;
24✔
933
        }
934

935
        /**
936
         * @param int $needle
937
         * @param int $scopeStart
938
         * @param int $scopeEnd
939
         *
940
         * @return bool
941
         */
942
        public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
24✔
943
        {
944
                return ($needle > $scopeStart && $needle < $scopeEnd);
24✔
945
        }
946

947
        /**
948
         * @param File $phpcsFile
949
         * @param int  $scopeStartIndex
950
         *
951
         * @return int
952
         */
953
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
356✔
954
        {
955
                $tokens = $phpcsFile->getTokens();
356✔
956
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
356✔
957
                if ($scopeStartIndex === 0) {
356✔
958
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
356✔
959
                }
89✔
960
                return $scopeCloserIndex;
356✔
961
        }
962

963
        /**
964
         * @param File $phpcsFile
965
         *
966
         * @return int
967
         */
968
        public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
356✔
969
        {
970
                $tokens = $phpcsFile->getTokens();
356✔
971
                foreach (array_reverse($tokens, true) as $index => $token) {
356✔
972
                        if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
356✔
973
                                return $index;
356✔
974
                        }
975
                }
87✔
976
                self::debug('no non-empty token found for end of file');
×
977
                return 0;
×
978
        }
979

980
        /**
981
         * @param VariableInfo $varInfo
982
         * @param ScopeInfo    $scopeInfo
983
         *
984
         * @return bool
985
         */
986
        public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo)
68✔
987
        {
988
                $foundVarPosition = false;
68✔
989
                foreach ($scopeInfo->variables as $variable) {
68✔
990
                        if ($variable === $varInfo) {
68✔
991
                                $foundVarPosition = true;
68✔
992
                                continue;
68✔
993
                        }
994
                        if (! $foundVarPosition) {
44✔
995
                                continue;
36✔
996
                        }
997
                        if ($variable->scopeType !== ScopeType::PARAM) {
44✔
998
                                continue;
36✔
999
                        }
1000
                        if ($variable->firstRead) {
16✔
1001
                                return true;
16✔
1002
                        }
1003
                }
17✔
1004
                return false;
68✔
1005
        }
1006

1007
        /**
1008
         * @param File         $phpcsFile
1009
         * @param VariableInfo $varInfo
1010
         * @param ScopeInfo    $scopeInfo
1011
         *
1012
         * @return bool
1013
         */
1014
        public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
4✔
1015
        {
1016
                $requireTokens = [
2✔
1017
                        T_REQUIRE,
4✔
1018
                        T_REQUIRE_ONCE,
4✔
1019
                        T_INCLUDE,
4✔
1020
                        T_INCLUDE_ONCE,
4✔
1021
                ];
3✔
1022
                $indexToStartSearch = $varInfo->firstDeclared;
4✔
1023
                if (! empty($varInfo->firstInitialized)) {
4✔
1024
                        $indexToStartSearch = $varInfo->firstInitialized;
4✔
1025
                }
1✔
1026
                $tokens = $phpcsFile->getTokens();
4✔
1027
                $indexToStopSearch = isset($tokens[$scopeInfo->scopeStartIndex]['scope_closer']) ? $tokens[$scopeInfo->scopeStartIndex]['scope_closer'] : null;
4✔
1028
                if (! is_int($indexToStartSearch) || ! is_int($indexToStopSearch)) {
4✔
1029
                        return false;
×
1030
                }
1031
                $requireTokenIndex = $phpcsFile->findNext($requireTokens, $indexToStartSearch + 1, $indexToStopSearch);
4✔
1032
                if (is_int($requireTokenIndex)) {
4✔
1033
                        return true;
4✔
1034
                }
1035
                return false;
×
1036
        }
1037

1038
        /**
1039
         * Find the index of the function keyword for a token in a function call's arguments
1040
         *
1041
         * For the variable `$foo` in the expression `doSomething($foo)`, this will
1042
         * return the index of the `doSomething` token.
1043
         *
1044
         * @param File $phpcsFile
1045
         * @param int  $stackPtr
1046
         *
1047
         * @return ?int
1048
         */
1049
        public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr)
324✔
1050
        {
1051
                $tokens = $phpcsFile->getTokens();
324✔
1052
                $token = $tokens[$stackPtr];
324✔
1053
                if (empty($token['nested_parenthesis'])) {
324✔
1054
                        return null;
316✔
1055
                }
1056
                /**
1057
                 * @var list<int|string>
1058
                 */
1059
                $startingParenthesis = array_keys($token['nested_parenthesis']);
80✔
1060
                $startOfArguments = end($startingParenthesis);
80✔
1061
                if (! is_int($startOfArguments)) {
80✔
1062
                        return null;
×
1063
                }
1064

1065
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
80✔
1066
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
80✔
1067
                if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
80✔
1068
                        return null;
×
1069
                }
1070
                if (
1071
                        $tokens[$functionPtr]['content'] === 'function'
80✔
1072
                        || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))
80✔
1073
                ) {
20✔
1074
                        // If there is a function/fn keyword before the beginning of the parens,
1075
                        // this is a function definition and not a function call.
1076
                        return null;
×
1077
                }
1078
                if (! empty($tokens[$functionPtr]['scope_opener'])) {
80✔
1079
                        // If the alleged function name has a scope, this is not a function call.
1080
                        return null;
28✔
1081
                }
1082

1083
                $functionNameType = $tokens[$functionPtr]['code'];
72✔
1084
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
72✔
1085
                        // If the alleged function name is not a variable or a string, this is
1086
                        // not a function call.
1087
                        return null;
32✔
1088
                }
1089

1090
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
56✔
1091
                        // If the variable is inside a different scope than the function name,
1092
                        // the function call doesn't apply to the variable.
1093
                        return null;
32✔
1094
                }
1095

1096
                return $functionPtr;
24✔
1097
        }
1098

1099
        /**
1100
         * @param File $phpcsFile
1101
         * @param int  $stackPtr
1102
         *
1103
         * @return bool
1104
         */
1105
        public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
332✔
1106
        {
1107
                // Use PHPCSUtils which handles all edge cases across PHP/PHPCS versions
1108
                return Context::inIsset($phpcsFile, $stackPtr) || Context::inEmpty($phpcsFile, $stackPtr);
332✔
1109
        }
1110

1111
        /**
1112
         * @param File $phpcsFile
1113
         * @param int  $stackPtr
1114
         *
1115
         * @return bool
1116
         */
1117
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
260✔
1118
        {
1119
                $tokens = $phpcsFile->getTokens();
260✔
1120
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
260✔
1121

1122
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
260✔
1123
                if (! is_int($arrayPushOperatorIndex1)) {
260✔
1124
                        return false;
×
1125
                }
1126
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
260✔
1127
                        return false;
260✔
1128
                }
1129

1130
                $arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true));
36✔
1131
                if (! is_int($arrayPushOperatorIndex2)) {
36✔
1132
                        return false;
×
1133
                }
1134
                if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') {
36✔
1135
                        return false;
8✔
1136
                }
1137

1138
                $arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true));
28✔
1139
                if (! is_int($arrayPushOperatorIndex3)) {
28✔
1140
                        return false;
×
1141
                }
1142
                if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') {
28✔
1143
                        return false;
×
1144
                }
1145

1146
                return true;
28✔
1147
        }
1148

1149
        /**
1150
         * @param File $phpcsFile
1151
         * @param int  $stackPtr
1152
         *
1153
         * @return bool
1154
         */
1155
        public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
260✔
1156
        {
1157
                // Use PHPCSUtils which handles all edge cases across PHP/PHPCS versions
1158
                return Context::inUnset($phpcsFile, $stackPtr);
260✔
1159
        }
1160

1161
        /**
1162
         * @param File $phpcsFile
1163
         * @param int  $stackPtr
1164
         *
1165
         * @return bool
1166
         */
1167
        public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr)
324✔
1168
        {
1169
                $previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
324✔
1170
                if (! is_int($previousStatementPtr)) {
324✔
1171
                        $previousStatementPtr = 1;
40✔
1172
                }
10✔
1173
                $previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
324✔
1174
                if (is_int($previousTokenPtr)) {
324✔
1175
                        return true;
4✔
1176
                }
1177
                return false;
324✔
1178
        }
1179

1180
        /**
1181
         * @param File $phpcsFile
1182
         * @param int  $stackPtr
1183
         *
1184
         * @return bool
1185
         */
1186
        public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
344✔
1187
        {
1188
                // Is the next non-whitespace an assignment?
1189
                $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
344✔
1190
                if (! is_int($assignPtr)) {
344✔
1191
                        return false;
336✔
1192
                }
1193

1194
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1195
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
324✔
1196
                        self::debug('found variable variable');
4✔
1197
                        return false;
4✔
1198
                }
1199
                return true;
324✔
1200
        }
1201

1202
        /**
1203
         * @param File $phpcsFile
1204
         * @param int  $stackPtr
1205
         *
1206
         * @return bool
1207
         */
1208
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
324✔
1209
        {
1210
                $tokens = $phpcsFile->getTokens();
324✔
1211

1212
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
324✔
1213
                if ($prev === false) {
324✔
1214
                        return false;
×
1215
                }
1216
                if ($tokens[$prev]['code'] === T_DOLLAR) {
324✔
1217
                        return true;
4✔
1218
                }
1219
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
324✔
1220
                        return false;
268✔
1221
                }
1222

1223
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
232✔
1224
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
232✔
1225
                        return true;
×
1226
                }
1227
                return false;
232✔
1228
        }
1229

1230
        /**
1231
         * @param File $phpcsFile
1232
         * @param int  $stackPtr
1233
         *
1234
         * @return EnumInfo|null
1235
         */
1236
        public static function makeEnumInfo(File $phpcsFile, $stackPtr)
4✔
1237
        {
1238
                $tokens = $phpcsFile->getTokens();
4✔
1239
                $token = $tokens[$stackPtr];
4✔
1240

1241
                if (isset($token['scope_opener'])) {
4✔
1242
                        $blockStart = $token['scope_opener'];
4✔
1243
                        $blockEnd = $token['scope_closer'];
4✔
1244
                } else {
1✔
1245
                        // Enums before phpcs could detect them do not have scopes so we have to
1246
                        // find them ourselves.
1247

1248
                        $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
4✔
1249
                        if (! is_int($blockStart)) {
4✔
1250
                                return null;
4✔
1251
                        }
UNCOV
1252
                        $blockEnd = $tokens[$blockStart]['bracket_closer'];
×
1253
                }
1254

1255
                return new EnumInfo(
4✔
1256
                        $stackPtr,
4✔
1257
                        $blockStart,
4✔
1258
                        $blockEnd
3✔
1259
                );
3✔
1260
        }
1261

1262
        /**
1263
         * @param File $phpcsFile
1264
         * @param int  $stackPtr
1265
         *
1266
         * @return ForLoopInfo
1267
         */
1268
        public static function makeForLoopInfo(File $phpcsFile, $stackPtr)
8✔
1269
        {
1270
                $tokens = $phpcsFile->getTokens();
8✔
1271
                $token = $tokens[$stackPtr];
8✔
1272
                $forIndex = $stackPtr;
8✔
1273
                $blockStart = $token['parenthesis_closer'];
8✔
1274
                if (isset($token['scope_opener'])) {
8✔
1275
                        $blockStart = $token['scope_opener'];
8✔
1276
                        $blockEnd = $token['scope_closer'];
8✔
1277
                } else {
2✔
1278
                        // Some for loop blocks will not have scope positions because it they are
1279
                        // inline (no curly braces) so we have to find the end of their scope by
1280
                        // looking for the end of the next statement.
1281
                        $nextSemicolonIndex = $phpcsFile->findNext([T_SEMICOLON], $token['parenthesis_closer']);
8✔
1282
                        if (! is_int($nextSemicolonIndex)) {
8✔
1283
                                $nextSemicolonIndex = $token['parenthesis_closer'] + 1;
×
1284
                        }
1285
                        $blockEnd = $nextSemicolonIndex;
8✔
1286
                }
1287
                $initStart = intval($token['parenthesis_opener']) + 1;
8✔
1288
                $initEnd = null;
8✔
1289
                $conditionStart = null;
8✔
1290
                $conditionEnd = null;
8✔
1291
                $incrementStart = null;
8✔
1292
                $incrementEnd = $token['parenthesis_closer'] - 1;
8✔
1293

1294
                $semicolonCount = 0;
8✔
1295
                $forLoopLevel = $tokens[$forIndex]['level'];
8✔
1296
                $forLoopNestedParensCount = 1;
8✔
1297

1298
                if (isset($tokens[$forIndex]['nested_parenthesis'])) {
8✔
1299
                        $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
×
1300
                }
1301

1302
                for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
8✔
1303
                        if ($tokens[$i]['code'] !== T_SEMICOLON) {
8✔
1304
                                continue;
8✔
1305
                        }
1306

1307
                        if ($tokens[$i]['level'] !== $forLoopLevel) {
8✔
1308
                                continue;
8✔
1309
                        }
1310

1311
                        if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
8✔
1312
                                continue;
×
1313
                        }
1314

1315
                        switch ($semicolonCount) {
1316
                                case 0:
8✔
1317
                                        $initEnd = $i;
8✔
1318
                                        $conditionStart = $initEnd + 1;
8✔
1319
                                        break;
8✔
1320
                                case 1:
8✔
1321
                                        $conditionEnd = $i;
8✔
1322
                                        $incrementStart = $conditionEnd + 1;
8✔
1323
                                        break;
8✔
1324
                        }
1325
                        $semicolonCount += 1;
8✔
1326
                }
2✔
1327

1328
                if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
8✔
1329
                        throw new \Exception("Cannot parse for loop at position {$forIndex}");
×
1330
                }
1331

1332
                return new ForLoopInfo(
8✔
1333
                        $forIndex,
8✔
1334
                        $blockStart,
8✔
1335
                        $blockEnd,
8✔
1336
                        $initStart,
8✔
1337
                        $initEnd,
8✔
1338
                        $conditionStart,
8✔
1339
                        $conditionEnd,
8✔
1340
                        $incrementStart,
8✔
1341
                        $incrementEnd
6✔
1342
                );
6✔
1343
        }
1344

1345
        /**
1346
         * @param int                     $stackPtr
1347
         * @param array<int, ForLoopInfo> $forLoops
1348
         * @return ForLoopInfo|null
1349
         */
1350
        public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
352✔
1351
        {
1352
                foreach ($forLoops as $forLoop) {
352✔
1353
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1354
                                return $forLoop;
8✔
1355
                        }
1356
                }
88✔
1357
                return null;
352✔
1358
        }
1359

1360
        /**
1361
         * Return true if the token looks like constructor promotion.
1362
         *
1363
         * Call on a parameter variable token only.
1364
         *
1365
         * @param File $phpcsFile
1366
         * @param int  $stackPtr
1367
         *
1368
         * @return bool
1369
         */
1370
        public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
232✔
1371
        {
1372
                $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
232✔
1373
                if (! $functionIndex) {
232✔
1374
                        return false;
×
1375
                }
1376
                $params = FunctionDeclarations::getParameters($phpcsFile, $functionIndex);
232✔
1377
                foreach ($params as $param) {
232✔
1378
                        if ($param['token'] === $stackPtr) {
232✔
1379
                                return isset($param['property_visibility']);
232✔
1380
                        }
1381
                }
40✔
UNCOV
1382
                return false;
×
1383
        }
1384

1385
        /**
1386
         * If looking at a function call token, return a string for the full function
1387
         * name including any inline namespace.
1388
         *
1389
         * So for example, if the call looks like `\My\Namespace\doSomething($bar)`
1390
         * and `$stackPtr` refers to `doSomething`, this will return
1391
         * `\My\Namespace\doSomething`.
1392
         *
1393
         * @param File $phpcsFile
1394
         * @param int  $stackPtr
1395
         *
1396
         * @return string|null
1397
         */
1398
        public static function getFunctionNameWithNamespace(File $phpcsFile, $stackPtr)
156✔
1399
        {
1400
                $tokens = $phpcsFile->getTokens();
156✔
1401

1402
                if (! isset($tokens[$stackPtr])) {
156✔
1403
                        return null;
×
1404
                }
1405
                $startOfScope = self::findVariableScope($phpcsFile, $stackPtr);
156✔
1406
                $functionName = $tokens[$stackPtr]['content'];
156✔
1407

1408
                // In PHPCS 4.x, T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, and T_NAME_RELATIVE
1409
                // tokens already contain the full namespaced name, so we can return early.
1410
                if ($tokens[$stackPtr]['code'] === T_NAME_FULLY_QUALIFIED) {
156✔
1411
                        return $functionName;
8✔
1412
                }
1413
                if ($tokens[$stackPtr]['code'] === T_NAME_QUALIFIED) {
156✔
1414
                        return $functionName;
×
1415
                }
1416
                if ($tokens[$stackPtr]['code'] === T_NAME_RELATIVE) {
156✔
1417
                        return $functionName;
×
1418
                }
1419

1420
                // Move backwards from the token, collecting namespace separators and
1421
                // strings, until we encounter whitespace or something else.
1422
                $partOfNamespace = [
78✔
1423
                        T_NS_SEPARATOR,
156✔
1424
                        T_STRING,
156✔
1425
                        T_NAME_QUALIFIED,
156✔
1426
                        T_NAME_RELATIVE,
156✔
1427
                        T_NAME_FULLY_QUALIFIED,
156✔
1428
                ];
117✔
1429
                for ($i = $stackPtr - 1; $i > $startOfScope; $i--) {
156✔
1430
                        if (! in_array($tokens[$i]['code'], $partOfNamespace, true)) {
156✔
1431
                                break;
156✔
1432
                        }
1433
                        $functionName = "{$tokens[$i]['content']}{$functionName}";
8✔
1434
                }
4✔
1435
                return $functionName;
156✔
1436
        }
1437

1438
        /**
1439
         * Return true if the token is inside an abstract class.
1440
         *
1441
         * @param File $phpcsFile
1442
         * @param int  $stackPtr
1443
         *
1444
         * @return bool
1445
         */
1446
        public static function isInAbstractClass(File $phpcsFile, $stackPtr)
108✔
1447
        {
1448
                $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
108✔
1449
                if (! is_int($classIndex)) {
108✔
1450
                        return false;
92✔
1451
                }
1452
                $classProperties = $phpcsFile->getClassProperties($classIndex);
16✔
1453
                return $classProperties['is_abstract'];
16✔
1454
        }
1455

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