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

sirbrillig / phpcs-variable-analysis / 18144700389

30 Sep 2025 10:05PM UTC coverage: 93.372% (-0.4%) from 93.79%
18144700389

Pull #356

github

sirbrillig
Update github actions to raise min phpcs version to 3.5.7
Pull Request #356: Update for phpcs 4.0.0

39 of 50 new or added lines in 2 files covered. (78.0%)

74 existing lines in 1 file now uncovered.

1930 of 2067 relevant lines covered (93.37%)

132.33 hits per line

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

89.24
/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

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

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

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

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

85
        /**
86
         * @param File $phpcsFile
87
         * @param int  $stackPtr
88
         *
89
         * @return ?int
90
         */
91
        public static function findContainingOpeningBracket(File $phpcsFile, $stackPtr)
352✔
92
        {
93
                $tokens = $phpcsFile->getTokens();
352✔
94
                if (isset($tokens[$stackPtr]['nested_parenthesis'])) {
352✔
95
                        /**
96
                         * @var list<int|string>
97
                         */
98
                        $openPtrs = array_keys($tokens[$stackPtr]['nested_parenthesis']);
260✔
99
                        return (int)end($openPtrs);
260✔
100
                }
101
                return null;
348✔
102
        }
103

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

389
                return $argPtrs;
36✔
390
        }
391

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

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

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

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

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

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

458
                return $enclosingScopeIndex;
356✔
459
        }
460

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

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

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

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

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

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

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

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

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

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

632
        /**
633
         * @param File $phpcsFile
634
         * @param int  $stackPtr
635
         *
636
         * @return bool
637
         */
UNCOV
638
        public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $stackPtr)
×
639
        {
UNCOV
640
                $tokens = $phpcsFile->getTokens();
×
UNCOV
641
                $token = $tokens[$stackPtr];
×
UNCOV
642
                $openParenIndices = isset($token['nested_parenthesis']) ? $token['nested_parenthesis'] : [];
×
UNCOV
643
                if (empty($openParenIndices)) {
×
UNCOV
644
                        return false;
×
645
                }
UNCOV
646
                $openParenPtr = $openParenIndices[0];
×
647
                return self::isArrowFunction($phpcsFile, $openParenPtr - 1);
×
648
        }
649

650
        /**
651
         * @param File $phpcsFile
652
         * @param int  $stackPtr
653
         * @param int  $enclosingScopeIndex
654
         *
655
         * @return ?int
656
         */
657
        public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
352✔
658
        {
659
                $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
352✔
660
                if (! is_int($arrowFunctionIndex)) {
352✔
661
                        return null;
352✔
662
                }
663
                $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex);
36✔
664
                if (! $arrowFunctionInfo) {
36✔
UNCOV
665
                        return null;
×
666
                }
667

668
                // We found the closest arrow function before this token. If the token is
669
                // within the scope of that arrow function, then return it.
670
                if ($stackPtr > $arrowFunctionInfo['scope_opener'] && $stackPtr < $arrowFunctionInfo['scope_closer']) {
36✔
671
                        return $arrowFunctionIndex;
36✔
672
                }
673

674
                // If the token is after the scope of the closest arrow function, we may
675
                // still be inside the scope of a nested arrow function, so we need to
676
                // search further back until we are certain there are no more arrow
677
                // functions.
678
                if ($stackPtr > $arrowFunctionInfo['scope_closer']) {
36✔
679
                        return self::getContainingArrowFunctionIndex($phpcsFile, $arrowFunctionIndex, $enclosingScopeIndex);
36✔
680
                }
681

UNCOV
682
                return null;
×
683
        }
684

685
        /**
686
         * Move back from the stackPtr to the start of the enclosing scope until we
687
         * find a 'fn' token that starts an arrow function, returning the index of
688
         * that token. Returns null if there are no arrow functions before stackPtr.
689
         *
690
         * Note that this does not guarantee that stackPtr is inside the arrow
691
         * function scope we find!
692
         *
693
         * @param File $phpcsFile
694
         * @param int  $stackPtr
695
         * @param int  $enclosingScopeIndex
696
         *
697
         * @return ?int
698
         */
699
        private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
352✔
700
        {
701
                $tokens = $phpcsFile->getTokens();
352✔
702
                for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) {
352✔
703
                        $token = $tokens[$index];
352✔
704
                        if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) {
352✔
705
                                return $index;
36✔
706
                        }
707
                }
88✔
708
                return null;
352✔
709
        }
710

711
        /**
712
         * @param File $phpcsFile
713
         * @param int  $stackPtr
714
         *
715
         * @return bool
716
         */
717
        public static function isArrowFunction(File $phpcsFile, $stackPtr)
356✔
718
        {
719
                $tokens = $phpcsFile->getTokens();
356✔
720
                if (defined('T_FN') && $tokens[$stackPtr]['code'] === T_FN) {
356✔
721
                        return true;
36✔
722
                }
723
                if ($tokens[$stackPtr]['content'] !== 'fn') {
356✔
724
                        return false;
356✔
725
                }
726
                // Make sure next non-space token is an open parenthesis
UNCOV
727
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
×
UNCOV
728
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
×
UNCOV
729
                        return false;
×
730
                }
731
                // Find the associated close parenthesis
UNCOV
732
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
×
733
                // Make sure the next token is a fat arrow
UNCOV
734
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
×
UNCOV
735
                if (! is_int($fatArrowIndex)) {
×
UNCOV
736
                        return false;
×
737
                }
UNCOV
738
                if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') {
×
UNCOV
739
                        return false;
×
740
                }
UNCOV
741
                return true;
×
742
        }
743

744
        /**
745
         * Find the opening and closing scope positions for an arrow function if the
746
         * given position is the start of the arrow function (the `fn` keyword
747
         * token).
748
         *
749
         * Returns null if the passed token is not an arrow function keyword.
750
         *
751
         * If the token is an arrow function keyword, the scope opener is returned as
752
         * the provided position.
753
         *
754
         * @param File $phpcsFile
755
         * @param int  $stackPtr
756
         *
757
         * @return ?array<string, int>
758
         */
759
        public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
36✔
760
        {
761
                $tokens = $phpcsFile->getTokens();
36✔
762
                if ($tokens[$stackPtr]['content'] !== 'fn') {
36✔
UNCOV
763
                        return null;
×
764
                }
765
                // Make sure next non-space token is an open parenthesis
766
                $openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
36✔
767
                if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
36✔
UNCOV
768
                        return null;
×
769
                }
770
                // Find the associated close parenthesis
771
                $closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
36✔
772
                // Make sure the next token is a fat arrow or a return type
773
                $fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
36✔
774
                if (! is_int($fatArrowIndex)) {
36✔
UNCOV
775
                        return null;
×
776
                }
777
                if (
778
                        $tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW &&
36✔
779
                        $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW' &&
36✔
780
                        $tokens[$fatArrowIndex]['code'] !== T_COLON
29✔
781
                ) {
9✔
UNCOV
782
                        return null;
×
783
                }
784

785
                // Find the scope closer
786
                $scopeCloserIndex = null;
36✔
787
                $foundCurlyPairs = 0;
36✔
788
                $foundArrayPairs = 0;
36✔
789
                $foundParenPairs = 0;
36✔
790
                $arrowBodyStart = $tokens[$stackPtr]['parenthesis_closer'] + 1;
36✔
791
                $lastToken = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
36✔
792
                for ($index = $arrowBodyStart; $index < $lastToken; $index++) {
36✔
793
                        $token = $tokens[$index];
36✔
794
                        if (empty($token['code'])) {
36✔
UNCOV
795
                                $scopeCloserIndex = $index;
×
UNCOV
796
                                break;
×
797
                        }
798

799
                        $code = $token['code'];
36✔
800

801
                        // A semicolon is always a closer.
802
                        if ($code === T_SEMICOLON) {
36✔
803
                                $scopeCloserIndex = $index;
28✔
804
                                break;
28✔
805
                        }
806

807
                        // Track pair opening tokens.
808
                        if ($code === T_OPEN_CURLY_BRACKET) {
36✔
809
                                $foundCurlyPairs += 1;
8✔
810
                                continue;
8✔
811
                        }
812
                        if ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) {
36✔
813
                                $foundArrayPairs += 1;
28✔
814
                                continue;
28✔
815
                        }
816
                        if ($code === T_OPEN_PARENTHESIS) {
36✔
817
                                $foundParenPairs += 1;
28✔
818
                                continue;
28✔
819
                        }
820

821
                        // A pair closing is only an arrow func closer if there was no matching opening token.
822
                        if ($code === T_CLOSE_CURLY_BRACKET) {
36✔
UNCOV
823
                                if ($foundCurlyPairs === 0) {
×
UNCOV
824
                                        $scopeCloserIndex = $index;
×
UNCOV
825
                                        break;
×
826
                                }
UNCOV
827
                                $foundCurlyPairs -= 1;
×
UNCOV
828
                                continue;
×
829
                        }
830
                        if ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) {
36✔
831
                                if ($foundArrayPairs === 0) {
28✔
UNCOV
832
                                        $scopeCloserIndex = $index;
×
UNCOV
833
                                        break;
×
834
                                }
835
                                $foundArrayPairs -= 1;
28✔
836
                                continue;
28✔
837
                        }
838
                        if ($code === T_CLOSE_PARENTHESIS) {
36✔
839
                                if ($foundParenPairs === 0) {
28✔
840
                                        $scopeCloserIndex = $index;
8✔
841
                                        break;
8✔
842
                                }
843
                                $foundParenPairs -= 1;
28✔
844
                                continue;
28✔
845
                        }
846

847
                        // A comma is a closer only if we are not inside an opening token.
848
                        if ($code === T_COMMA) {
36✔
849
                                if (empty($foundArrayPairs) && empty($foundParenPairs) && empty($foundCurlyPairs)) {
28✔
850
                                        $scopeCloserIndex = $index;
16✔
851
                                        break;
16✔
852
                                }
853
                                continue;
20✔
854
                        }
855
                }
9✔
856

857
                if (! is_int($scopeCloserIndex)) {
36✔
UNCOV
858
                        return null;
×
859
                }
860

861
                return [
18✔
862
                        'scope_opener' => $stackPtr,
36✔
863
                        'scope_closer' => $scopeCloserIndex,
36✔
864
                ];
27✔
865
        }
866

867
        /**
868
         * Determine if a token is a list opener for list assignment/destructuring.
869
         *
870
         * The index provided can be either the opening square brace of a short list
871
         * assignment like the first character of `[$a] = $b;` or the `list` token of
872
         * an expression like `list($a) = $b;` or the opening parenthesis of that
873
         * expression.
874
         *
875
         * @param File $phpcsFile
876
         * @param int  $listOpenerIndex
877
         *
878
         * @return bool
879
         */
880
        private static function isListAssignment(File $phpcsFile, $listOpenerIndex)
32✔
881
        {
882
                $tokens = $phpcsFile->getTokens();
32✔
883
                // Match `[$a] = $b;` except for when the previous token is a parenthesis.
884
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SHORT_ARRAY) {
32✔
885
                        return true;
28✔
886
                }
887
                // Match `list($a) = $b;`
888
                if ($tokens[$listOpenerIndex]['code'] === T_LIST) {
32✔
889
                        return true;
32✔
890
                }
891

892
                // If $listOpenerIndex is the open parenthesis of `list($a) = $b;`, then
893
                // match that too.
894
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_PARENTHESIS) {
16✔
895
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
4✔
896
                        if (
897
                                isset($tokens[$previousTokenPtr])
4✔
898
                                && $tokens[$previousTokenPtr]['code'] === T_LIST
4✔
899
                        ) {
1✔
900
                                return true;
4✔
901
                        }
UNCOV
902
                        return true;
×
903
                }
904

905
                // If the list opener token is a square bracket that is preceeded by a
906
                // close parenthesis that has an owner which is a scope opener, then this
907
                // is a list assignment and not an array access.
908
                //
909
                // Match `if (true) [$a] = $b;`
910
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SQUARE_BRACKET) {
12✔
911
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
12✔
912
                        if (
913
                                isset($tokens[$previousTokenPtr])
12✔
914
                                && $tokens[$previousTokenPtr]['code'] === T_CLOSE_PARENTHESIS
12✔
915
                                && isset($tokens[$previousTokenPtr]['parenthesis_owner'])
12✔
916
                                && isset(Tokens::$scopeOpeners[$tokens[$tokens[$previousTokenPtr]['parenthesis_owner']]['code']])
12✔
917
                        ) {
4✔
918
                                return true;
4✔
919
                        }
920
                }
2✔
921

922
                return false;
8✔
923
        }
924

925
        /**
926
         * Return a list of indices for variables assigned within a list assignment.
927
         *
928
         * The index provided can be either the opening square brace of a short list
929
         * assignment like the first character of `[$a] = $b;` or the `list` token of
930
         * an expression like `list($a) = $b;` or the opening parenthesis of that
931
         * expression.
932
         *
933
         * @param File $phpcsFile
934
         * @param int  $listOpenerIndex
935
         *
936
         * @return ?array<int>
937
         */
938
        public static function getListAssignments(File $phpcsFile, $listOpenerIndex)
48✔
939
        {
940
                $tokens = $phpcsFile->getTokens();
48✔
941
                self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]);
48✔
942

943
                // First find the end of the list
944
                $closePtr = null;
48✔
945
                if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) {
48✔
946
                        $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer'];
48✔
947
                }
12✔
948
                if (isset($tokens[$listOpenerIndex]['bracket_closer'])) {
48✔
949
                        $closePtr = $tokens[$listOpenerIndex]['bracket_closer'];
48✔
950
                }
12✔
951
                if (! $closePtr) {
48✔
UNCOV
952
                        return null;
×
953
                }
954

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

958
                // If the next token isn't an assignment, check for nested brackets because we might be a nested assignment
959
                if (! is_int($assignPtr) || $tokens[$assignPtr]['code'] !== T_EQUAL) {
48✔
960
                        // Collect the enclosing list open/close tokens ($parents is an assoc array keyed by opener index and the value is the closer index)
961
                        $parents = isset($tokens[$listOpenerIndex]['nested_parenthesis']) ? $tokens[$listOpenerIndex]['nested_parenthesis'] : [];
40✔
962
                        // There's no record of nested brackets for short lists; we'll have to find the parent ourselves
963
                        if (empty($parents)) {
40✔
964
                                $parentSquareBracketPtr = self::findContainingOpeningSquareBracket($phpcsFile, $listOpenerIndex);
40✔
965
                                if (is_int($parentSquareBracketPtr)) {
40✔
966
                                        // Make sure that the parent is really a parent by checking that its
967
                                        // closing index is outside of the current bracket's closing index.
968
                                        $parentSquareBracketToken = $tokens[$parentSquareBracketPtr];
4✔
969
                                        $parentSquareBracketClosePtr = $parentSquareBracketToken['bracket_closer'];
4✔
970
                                        if ($parentSquareBracketClosePtr && $parentSquareBracketClosePtr > $closePtr) {
4✔
971
                                                self::debug("found enclosing bracket for {$listOpenerIndex}: {$parentSquareBracketPtr}");
4✔
972
                                                // Collect the opening index, but we don't actually need the closing paren index so just make that 0
973
                                                $parents = [$parentSquareBracketPtr => 0];
4✔
974
                                        }
1✔
975
                                }
1✔
976
                        }
10✔
977
                        // If we have no parents, this is not a nested assignment and therefore is not an assignment
978
                        if (empty($parents)) {
40✔
979
                                return null;
40✔
980
                        }
981

982
                        // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion)
983
                        $isNestedAssignment = null;
28✔
984
                        $parentListOpener = array_keys(array_reverse($parents, true))[0];
28✔
985
                        $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener);
28✔
986
                        if ($isNestedAssignment === null) {
28✔
987
                                return null;
28✔
988
                        }
989
                }
1✔
990

991
                $variablePtrs = [];
32✔
992

993
                $currentPtr = $listOpenerIndex;
32✔
994
                $variablePtr = 0;
32✔
995
                while ($currentPtr < $closePtr && is_int($variablePtr)) {
32✔
996
                        $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr);
32✔
997
                        if (is_int($variablePtr)) {
32✔
998
                                $variablePtrs[] = $variablePtr;
32✔
999
                        }
8✔
1000
                        ++$currentPtr;
32✔
1001
                }
8✔
1002

1003
                if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) {
32✔
1004
                        return null;
8✔
1005
                }
1006

1007
                return $variablePtrs;
32✔
1008
        }
1009

1010
        /**
1011
         * @param File $phpcsFile
1012
         * @param int  $stackPtr
1013
         *
1014
         * @return string[]
1015
         */
1016
        public static function getVariablesDefinedByArrowFunction(File $phpcsFile, $stackPtr)
36✔
1017
        {
1018
                $tokens = $phpcsFile->getTokens();
36✔
1019
                $arrowFunctionToken = $tokens[$stackPtr];
36✔
1020
                $variableNames = [];
36✔
1021
                self::debug('looking for variables in arrow function token', $arrowFunctionToken);
36✔
1022
                for ($index = $arrowFunctionToken['parenthesis_opener']; $index < $arrowFunctionToken['parenthesis_closer']; $index++) {
36✔
1023
                        $token = $tokens[$index];
36✔
1024
                        if ($token['code'] === T_VARIABLE) {
36✔
1025
                                $variableNames[] = self::normalizeVarName($token['content']);
36✔
1026
                        }
9✔
1027
                }
9✔
1028
                self::debug('found these variables in arrow function token', $variableNames);
36✔
1029
                return $variableNames;
36✔
1030
        }
1031

1032
        /**
1033
         * @return void
1034
         */
1035
        public static function debug()
356✔
1036
        {
1037
                $messages = func_get_args();
356✔
1038
                if (! defined('PHP_CODESNIFFER_VERBOSITY')) {
356✔
UNCOV
1039
                        return;
×
1040
                }
1041
                if (PHP_CODESNIFFER_VERBOSITY <= 3) {
356✔
1042
                        return;
356✔
1043
                }
UNCOV
1044
                $output = PHP_EOL . 'VariableAnalysisSniff: DEBUG:';
×
UNCOV
1045
                foreach ($messages as $message) {
×
UNCOV
1046
                        if (is_string($message) || is_numeric($message)) {
×
1047
                                $output .= ' "' . $message . '"';
×
UNCOV
1048
                                continue;
×
1049
                        }
UNCOV
1050
                        $output .= PHP_EOL . var_export($message, true) . PHP_EOL;
×
1051
                }
UNCOV
1052
                $output .= PHP_EOL;
×
UNCOV
1053
                echo $output;
×
1054
        }
1055

1056
        /**
1057
         * @param string $pattern
1058
         * @param string $value
1059
         *
1060
         * @return string[]
1061
         */
1062
        public static function splitStringToArray($pattern, $value)
28✔
1063
        {
1064
                if (empty($pattern)) {
28✔
UNCOV
1065
                        return [];
×
1066
                }
1067
                $result = preg_split($pattern, $value);
28✔
1068
                return is_array($result) ? $result : [];
28✔
1069
        }
1070

1071
        /**
1072
         * @param string $varName
1073
         *
1074
         * @return bool
1075
         */
1076
        public static function isVariableANumericVariable($varName)
340✔
1077
        {
1078
                return is_numeric(substr($varName, 0, 1));
340✔
1079
        }
1080

1081
        /**
1082
         * @param File $phpcsFile
1083
         * @param int  $stackPtr
1084
         *
1085
         * @return bool
1086
         */
1087
        public static function isVariableInsideElseCondition(File $phpcsFile, $stackPtr)
332✔
1088
        {
1089
                $tokens = $phpcsFile->getTokens();
332✔
1090
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
332✔
1091
                $nonFunctionTokenTypes[] = T_OPEN_PARENTHESIS;
332✔
1092
                $nonFunctionTokenTypes[] = T_INLINE_HTML;
332✔
1093
                $nonFunctionTokenTypes[] = T_CLOSE_TAG;
332✔
1094
                $nonFunctionTokenTypes[] = T_VARIABLE;
332✔
1095
                $nonFunctionTokenTypes[] = T_ELLIPSIS;
332✔
1096
                $nonFunctionTokenTypes[] = T_COMMA;
332✔
1097
                $nonFunctionTokenTypes[] = T_STRING;
332✔
1098
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
332✔
1099
                $elsePtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $stackPtr - 1, null, true, null, true));
332✔
1100
                $elseTokenTypes = [
166✔
1101
                        T_ELSE,
332✔
1102
                        T_ELSEIF,
332✔
1103
                ];
249✔
1104
                if (is_int($elsePtr) && in_array($tokens[$elsePtr]['code'], $elseTokenTypes, true)) {
332✔
1105
                        return true;
16✔
1106
                }
1107
                return false;
332✔
1108
        }
1109

1110
        /**
1111
         * @param File $phpcsFile
1112
         * @param int  $stackPtr
1113
         *
1114
         * @return bool
1115
         */
1116
        public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr)
332✔
1117
        {
1118
                $tokens = $phpcsFile->getTokens();
332✔
1119
                $token = $tokens[$stackPtr];
332✔
1120
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
332✔
1121
                $elseTokenTypes = [
166✔
1122
                        T_ELSE,
332✔
1123
                        T_ELSEIF,
332✔
1124
                ];
249✔
1125
                foreach (array_reverse($conditions, true) as $scopeCode) {
332✔
1126
                        if (in_array($scopeCode, $elseTokenTypes, true)) {
312✔
1127
                                return true;
16✔
1128
                        }
1129
                }
83✔
1130

1131
                // Some else body code will not have conditions because it is inline (no
1132
                // curly braces) so we have to look in other ways.
1133
                $previousSemicolonPtr = $phpcsFile->findPrevious([T_SEMICOLON], $stackPtr - 1);
332✔
1134
                if (! is_int($previousSemicolonPtr)) {
332✔
1135
                        $previousSemicolonPtr = 0;
152✔
1136
                }
38✔
1137
                $elsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1, $previousSemicolonPtr);
332✔
1138
                if (is_int($elsePtr)) {
332✔
1139
                        return true;
8✔
1140
                }
1141

1142
                return false;
332✔
1143
        }
1144

1145
        /**
1146
         * @param File $phpcsFile
1147
         * @param int  $stackPtr
1148
         *
1149
         * @return int[]
1150
         */
1151
        public static function getAttachedBlockIndicesForElse(File $phpcsFile, $stackPtr)
24✔
1152
        {
1153
                $currentElsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1);
24✔
1154
                if (! is_int($currentElsePtr)) {
24✔
UNCOV
1155
                        throw new \Exception("Cannot find expected else at {$stackPtr}");
×
1156
                }
1157

1158
                $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
24✔
1159
                if (! is_int($ifPtr)) {
24✔
UNCOV
1160
                        throw new \Exception("Cannot find if for else at {$stackPtr}");
×
1161
                }
1162
                $blockIndices = [$ifPtr];
24✔
1163

1164
                $previousElseIfPtr = $currentElsePtr;
24✔
1165
                do {
1166
                        $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
24✔
1167
                        if (is_int($elseIfPtr)) {
24✔
1168
                                $blockIndices[] = $elseIfPtr;
16✔
1169
                                $previousElseIfPtr = $elseIfPtr;
16✔
1170
                        }
4✔
1171
                } while (is_int($elseIfPtr));
24✔
1172

1173
                return $blockIndices;
24✔
1174
        }
1175

1176
        /**
1177
         * @param int $needle
1178
         * @param int $scopeStart
1179
         * @param int $scopeEnd
1180
         *
1181
         * @return bool
1182
         */
1183
        public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
24✔
1184
        {
1185
                return ($needle > $scopeStart && $needle < $scopeEnd);
24✔
1186
        }
1187

1188
        /**
1189
         * @param File $phpcsFile
1190
         * @param int  $scopeStartIndex
1191
         *
1192
         * @return int
1193
         */
1194
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
356✔
1195
        {
1196
                $tokens = $phpcsFile->getTokens();
356✔
1197
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
356✔
1198

1199
                if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) {
356✔
1200
                        $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex);
36✔
1201
                        $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex;
36✔
1202
                }
9✔
1203

1204
                if ($scopeStartIndex === 0) {
356✔
1205
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
356✔
1206
                }
89✔
1207
                return $scopeCloserIndex;
356✔
1208
        }
1209

1210
        /**
1211
         * @param File $phpcsFile
1212
         *
1213
         * @return int
1214
         */
1215
        public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
356✔
1216
        {
1217
                $tokens = $phpcsFile->getTokens();
356✔
1218
                foreach (array_reverse($tokens, true) as $index => $token) {
356✔
1219
                        if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
356✔
1220
                                return $index;
356✔
1221
                        }
1222
                }
87✔
UNCOV
1223
                self::debug('no non-empty token found for end of file');
×
UNCOV
1224
                return 0;
×
1225
        }
1226

1227
        /**
1228
         * @param VariableInfo $varInfo
1229
         * @param ScopeInfo    $scopeInfo
1230
         *
1231
         * @return bool
1232
         */
1233
        public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo)
68✔
1234
        {
1235
                $foundVarPosition = false;
68✔
1236
                foreach ($scopeInfo->variables as $variable) {
68✔
1237
                        if ($variable === $varInfo) {
68✔
1238
                                $foundVarPosition = true;
68✔
1239
                                continue;
68✔
1240
                        }
1241
                        if (! $foundVarPosition) {
44✔
1242
                                continue;
36✔
1243
                        }
1244
                        if ($variable->scopeType !== ScopeType::PARAM) {
44✔
1245
                                continue;
36✔
1246
                        }
1247
                        if ($variable->firstRead) {
16✔
1248
                                return true;
16✔
1249
                        }
1250
                }
17✔
1251
                return false;
68✔
1252
        }
1253

1254
        /**
1255
         * @param File         $phpcsFile
1256
         * @param VariableInfo $varInfo
1257
         * @param ScopeInfo    $scopeInfo
1258
         *
1259
         * @return bool
1260
         */
1261
        public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
4✔
1262
        {
1263
                $requireTokens = [
2✔
1264
                        T_REQUIRE,
4✔
1265
                        T_REQUIRE_ONCE,
4✔
1266
                        T_INCLUDE,
4✔
1267
                        T_INCLUDE_ONCE,
4✔
1268
                ];
3✔
1269
                $indexToStartSearch = $varInfo->firstDeclared;
4✔
1270
                if (! empty($varInfo->firstInitialized)) {
4✔
1271
                        $indexToStartSearch = $varInfo->firstInitialized;
4✔
1272
                }
1✔
1273
                $tokens = $phpcsFile->getTokens();
4✔
1274
                $indexToStopSearch = isset($tokens[$scopeInfo->scopeStartIndex]['scope_closer']) ? $tokens[$scopeInfo->scopeStartIndex]['scope_closer'] : null;
4✔
1275
                if (! is_int($indexToStartSearch) || ! is_int($indexToStopSearch)) {
4✔
UNCOV
1276
                        return false;
×
1277
                }
1278
                $requireTokenIndex = $phpcsFile->findNext($requireTokens, $indexToStartSearch + 1, $indexToStopSearch);
4✔
1279
                if (is_int($requireTokenIndex)) {
4✔
1280
                        return true;
4✔
1281
                }
UNCOV
1282
                return false;
×
1283
        }
1284

1285
        /**
1286
         * Find the index of the function keyword for a token in a function call's arguments
1287
         *
1288
         * For the variable `$foo` in the expression `doSomething($foo)`, this will
1289
         * return the index of the `doSomething` token.
1290
         *
1291
         * @param File $phpcsFile
1292
         * @param int  $stackPtr
1293
         *
1294
         * @return ?int
1295
         */
1296
        public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr)
344✔
1297
        {
1298
                $tokens = $phpcsFile->getTokens();
344✔
1299
                $token = $tokens[$stackPtr];
344✔
1300
                if (empty($token['nested_parenthesis'])) {
344✔
1301
                        return null;
336✔
1302
                }
1303
                /**
1304
                 * @var list<int|string>
1305
                 */
1306
                $startingParenthesis = array_keys($token['nested_parenthesis']);
216✔
1307
                $startOfArguments = end($startingParenthesis);
216✔
1308
                if (! is_int($startOfArguments)) {
216✔
UNCOV
1309
                        return null;
×
1310
                }
1311

1312
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
216✔
1313
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
216✔
1314
                if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
216✔
UNCOV
1315
                        return null;
×
1316
                }
1317
                if (
1318
                        $tokens[$functionPtr]['content'] === 'function'
216✔
1319
                        || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))
216✔
1320
                ) {
54✔
1321
                        // If there is a function/fn keyword before the beginning of the parens,
1322
                        // this is a function definition and not a function call.
UNCOV
1323
                        return null;
×
1324
                }
1325
                if (! empty($tokens[$functionPtr]['scope_opener'])) {
216✔
1326
                        // If the alleged function name has a scope, this is not a function call.
1327
                        return null;
142✔
1328
                }
1329

1330
                $functionNameType = $tokens[$functionPtr]['code'];
182✔
1331
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
182✔
1332
                        // If the alleged function name is not a variable or a string, this is
1333
                        // not a function call.
1334
                        return null;
48✔
1335
                }
1336

1337
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
178✔
1338
                        // If the variable is inside a different scope than the function name,
1339
                        // the function call doesn't apply to the variable.
1340
                        return null;
32✔
1341
                }
1342

1343
                return $functionPtr;
178✔
1344
        }
1345

1346
        /**
1347
         * @param File $phpcsFile
1348
         * @param int  $stackPtr
1349
         *
1350
         * @return bool
1351
         */
1352
        public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
332✔
1353
        {
1354
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
332✔
1355
                if (! is_int($functionIndex)) {
332✔
1356
                        return false;
314✔
1357
                }
1358
                $tokens = $phpcsFile->getTokens();
178✔
1359
                if (! isset($tokens[$functionIndex])) {
178✔
UNCOV
1360
                        return false;
×
1361
                }
1362
                $allowedFunctionNames = [
89✔
1363
                        'isset',
178✔
1364
                        'empty',
134✔
1365
                ];
134✔
1366
                if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) {
178✔
1367
                        return true;
20✔
1368
                }
1369
                return false;
170✔
1370
        }
1371

1372
        /**
1373
         * @param File $phpcsFile
1374
         * @param int  $stackPtr
1375
         *
1376
         * @return bool
1377
         */
1378
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
260✔
1379
        {
1380
                $tokens = $phpcsFile->getTokens();
260✔
1381
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
260✔
1382

1383
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
260✔
1384
                if (! is_int($arrayPushOperatorIndex1)) {
260✔
UNCOV
1385
                        return false;
×
1386
                }
1387
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
260✔
1388
                        return false;
260✔
1389
                }
1390

1391
                $arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true));
36✔
1392
                if (! is_int($arrayPushOperatorIndex2)) {
36✔
UNCOV
1393
                        return false;
×
1394
                }
1395
                if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') {
36✔
1396
                        return false;
8✔
1397
                }
1398

1399
                $arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true));
28✔
1400
                if (! is_int($arrayPushOperatorIndex3)) {
28✔
UNCOV
1401
                        return false;
×
1402
                }
1403
                if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') {
28✔
UNCOV
1404
                        return false;
×
1405
                }
1406

1407
                return true;
28✔
1408
        }
1409

1410
        /**
1411
         * @param File $phpcsFile
1412
         * @param int  $stackPtr
1413
         *
1414
         * @return bool
1415
         */
1416
        public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
260✔
1417
        {
1418
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
260✔
1419
                if (! is_int($functionIndex)) {
260✔
1420
                        return false;
230✔
1421
                }
1422
                $tokens = $phpcsFile->getTokens();
102✔
1423
                if (! isset($tokens[$functionIndex])) {
102✔
UNCOV
1424
                        return false;
×
1425
                }
1426
                if ($tokens[$functionIndex]['content'] === 'unset') {
102✔
1427
                        return true;
8✔
1428
                }
1429
                return false;
94✔
1430
        }
1431

1432
        /**
1433
         * @param File $phpcsFile
1434
         * @param int  $stackPtr
1435
         *
1436
         * @return bool
1437
         */
1438
        public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr)
324✔
1439
        {
1440
                $previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
324✔
1441
                if (! is_int($previousStatementPtr)) {
324✔
1442
                        $previousStatementPtr = 1;
40✔
1443
                }
10✔
1444
                $previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
324✔
1445
                if (is_int($previousTokenPtr)) {
324✔
1446
                        return true;
4✔
1447
                }
1448
                return false;
324✔
1449
        }
1450

1451
        /**
1452
         * @param File $phpcsFile
1453
         * @param int  $stackPtr
1454
         *
1455
         * @return bool
1456
         */
1457
        public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
344✔
1458
        {
1459
                // Is the next non-whitespace an assignment?
1460
                $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
344✔
1461
                if (! is_int($assignPtr)) {
344✔
1462
                        return false;
336✔
1463
                }
1464

1465
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1466
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
324✔
1467
                        self::debug('found variable variable');
4✔
1468
                        return false;
4✔
1469
                }
1470
                return true;
324✔
1471
        }
1472

1473
        /**
1474
         * @param File $phpcsFile
1475
         * @param int  $stackPtr
1476
         *
1477
         * @return bool
1478
         */
1479
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
324✔
1480
        {
1481
                $tokens = $phpcsFile->getTokens();
324✔
1482

1483
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
324✔
1484
                if ($prev === false) {
324✔
UNCOV
1485
                        return false;
×
1486
                }
1487
                if ($tokens[$prev]['code'] === T_DOLLAR) {
324✔
1488
                        return true;
4✔
1489
                }
1490
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
324✔
1491
                        return false;
268✔
1492
                }
1493

1494
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
232✔
1495
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
232✔
UNCOV
1496
                        return true;
×
1497
                }
1498
                return false;
232✔
1499
        }
1500

1501
        /**
1502
         * @param File $phpcsFile
1503
         * @param int  $stackPtr
1504
         *
1505
         * @return EnumInfo|null
1506
         */
1507
        public static function makeEnumInfo(File $phpcsFile, $stackPtr)
4✔
1508
        {
1509
                $tokens = $phpcsFile->getTokens();
4✔
1510
                $token = $tokens[$stackPtr];
4✔
1511

1512
                if (isset($token['scope_opener'])) {
4✔
1513
                        $blockStart = $token['scope_opener'];
2✔
1514
                        $blockEnd = $token['scope_closer'];
2✔
1515
                } else {
1516
                        // Enums before phpcs could detect them do not have scopes so we have to
1517
                        // find them ourselves.
1518

1519
                        $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
4✔
1520
                        if (! is_int($blockStart)) {
4✔
1521
                                return null;
4✔
1522
                        }
1523
                        $blockEnd = $tokens[$blockStart]['bracket_closer'];
2✔
1524
                }
1525

1526
                return new EnumInfo(
4✔
1527
                        $stackPtr,
4✔
1528
                        $blockStart,
4✔
1529
                        $blockEnd
3✔
1530
                );
3✔
1531
        }
1532

1533
        /**
1534
         * @param File $phpcsFile
1535
         * @param int  $stackPtr
1536
         *
1537
         * @return ForLoopInfo
1538
         */
1539
        public static function makeForLoopInfo(File $phpcsFile, $stackPtr)
8✔
1540
        {
1541
                $tokens = $phpcsFile->getTokens();
8✔
1542
                $token = $tokens[$stackPtr];
8✔
1543
                $forIndex = $stackPtr;
8✔
1544
                $blockStart = $token['parenthesis_closer'];
8✔
1545
                if (isset($token['scope_opener'])) {
8✔
1546
                        $blockStart = $token['scope_opener'];
8✔
1547
                        $blockEnd = $token['scope_closer'];
8✔
1548
                } else {
2✔
1549
                        // Some for loop blocks will not have scope positions because it they are
1550
                        // inline (no curly braces) so we have to find the end of their scope by
1551
                        // looking for the end of the next statement.
1552
                        $nextSemicolonIndex = $phpcsFile->findNext([T_SEMICOLON], $token['parenthesis_closer']);
8✔
1553
                        if (! is_int($nextSemicolonIndex)) {
8✔
UNCOV
1554
                                $nextSemicolonIndex = $token['parenthesis_closer'] + 1;
×
1555
                        }
1556
                        $blockEnd = $nextSemicolonIndex;
8✔
1557
                }
1558
                $initStart = intval($token['parenthesis_opener']) + 1;
8✔
1559
                $initEnd = null;
8✔
1560
                $conditionStart = null;
8✔
1561
                $conditionEnd = null;
8✔
1562
                $incrementStart = null;
8✔
1563
                $incrementEnd = $token['parenthesis_closer'] - 1;
8✔
1564

1565
                $semicolonCount = 0;
8✔
1566
                $forLoopLevel = $tokens[$forIndex]['level'];
8✔
1567
                $forLoopNestedParensCount = 1;
8✔
1568

1569
                if (isset($tokens[$forIndex]['nested_parenthesis'])) {
8✔
UNCOV
1570
                        $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
×
1571
                }
1572

1573
                for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
8✔
1574
                        if ($tokens[$i]['code'] !== T_SEMICOLON) {
8✔
1575
                                continue;
8✔
1576
                        }
1577

1578
                        if ($tokens[$i]['level'] !== $forLoopLevel) {
8✔
1579
                                continue;
8✔
1580
                        }
1581

1582
                        if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
8✔
UNCOV
1583
                                continue;
×
1584
                        }
1585

1586
                        switch ($semicolonCount) {
1587
                                case 0:
8✔
1588
                                        $initEnd = $i;
8✔
1589
                                        $conditionStart = $initEnd + 1;
8✔
1590
                                        break;
8✔
1591
                                case 1:
8✔
1592
                                        $conditionEnd = $i;
8✔
1593
                                        $incrementStart = $conditionEnd + 1;
8✔
1594
                                        break;
8✔
1595
                        }
1596
                        $semicolonCount += 1;
8✔
1597
                }
2✔
1598

1599
                if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
8✔
UNCOV
1600
                        throw new \Exception("Cannot parse for loop at position {$forIndex}");
×
1601
                }
1602

1603
                return new ForLoopInfo(
8✔
1604
                        $forIndex,
8✔
1605
                        $blockStart,
8✔
1606
                        $blockEnd,
8✔
1607
                        $initStart,
8✔
1608
                        $initEnd,
8✔
1609
                        $conditionStart,
8✔
1610
                        $conditionEnd,
8✔
1611
                        $incrementStart,
8✔
1612
                        $incrementEnd
6✔
1613
                );
6✔
1614
        }
1615

1616
        /**
1617
         * @param int                     $stackPtr
1618
         * @param array<int, ForLoopInfo> $forLoops
1619
         * @return ForLoopInfo|null
1620
         */
1621
        public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
352✔
1622
        {
1623
                foreach ($forLoops as $forLoop) {
352✔
1624
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1625
                                return $forLoop;
8✔
1626
                        }
1627
                }
88✔
1628
                return null;
352✔
1629
        }
1630

1631
        /**
1632
         * Return true if the token looks like constructor promotion.
1633
         *
1634
         * Call on a parameter variable token only.
1635
         *
1636
         * @param File $phpcsFile
1637
         * @param int  $stackPtr
1638
         *
1639
         * @return bool
1640
         */
1641
        public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
232✔
1642
        {
1643
                // If we are not in a function's parameters, this is not promotion.
1644
                $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
232✔
1645
                if (! $functionIndex) {
232✔
UNCOV
1646
                        return false;
×
1647
                }
1648

1649
                $tokens = $phpcsFile->getTokens();
232✔
1650

1651
                // Move backwards from the token, ignoring whitespace, typehints, and the
1652
                // 'readonly' keyword, and return true if the previous token is a
1653
                // visibility keyword (eg: `public`).
1654
                for ($i = $stackPtr - 1; $i > $functionIndex; $i--) {
232✔
1655
                        if (in_array($tokens[$i]['code'], Tokens::$scopeModifiers, true)) {
232✔
1656
                                return true;
12✔
1657
                        }
1658
                        if (in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) {
232✔
1659
                                continue;
160✔
1660
                        }
1661
                        if ($tokens[$i]['content'] === 'readonly') {
232✔
1662
                                continue;
8✔
1663
                        }
1664
                        if (self::isTokenPartOfTypehint($phpcsFile, $i)) {
232✔
1665
                                continue;
32✔
1666
                        }
1667
                        return false;
228✔
1668
                }
UNCOV
1669
                return false;
×
1670
        }
1671

1672
        /**
1673
         * If looking at a function call token, return a string for the full function
1674
         * name including any inline namespace.
1675
         *
1676
         * So for example, if the call looks like `\My\Namespace\doSomething($bar)`
1677
         * and `$stackPtr` refers to `doSomething`, this will return
1678
         * `\My\Namespace\doSomething`.
1679
         *
1680
         * @param File $phpcsFile
1681
         * @param int  $stackPtr
1682
         *
1683
         * @return string|null
1684
         */
1685
        public static function getFunctionNameWithNamespace(File $phpcsFile, $stackPtr)
158✔
1686
        {
1687
                $tokens = $phpcsFile->getTokens();
158✔
1688

1689
                if (! isset($tokens[$stackPtr])) {
158✔
UNCOV
1690
                        return null;
×
1691
                }
1692
                $startOfScope = self::findVariableScope($phpcsFile, $stackPtr);
158✔
1693
                $functionName = $tokens[$stackPtr]['content'];
158✔
1694

1695
                // In PHPCS 4.x, T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, and T_NAME_RELATIVE
1696
                // tokens already contain the full namespaced name, so we can return early.
1697
                if ($tokens[$stackPtr]['code'] === T_NAME_FULLY_QUALIFIED) {
158✔
1698
                        return $functionName;
8✔
1699
                }
1700
                if ($tokens[$stackPtr]['code'] === T_NAME_QUALIFIED) {
158✔
NEW
UNCOV
1701
                        return $functionName;
×
1702
                }
1703
                if ($tokens[$stackPtr]['code'] === T_NAME_RELATIVE) {
158✔
NEW
UNCOV
1704
                        return $functionName;
×
1705
                }
1706

1707
                // Move backwards from the token, collecting namespace separators and
1708
                // strings, until we encounter whitespace or something else.
1709
                $partOfNamespace = [
79✔
1710
                        T_NS_SEPARATOR,
158✔
1711
                        T_STRING,
158✔
1712
                        T_NAME_QUALIFIED,
158✔
1713
                        T_NAME_RELATIVE,
158✔
1714
                        T_NAME_FULLY_QUALIFIED,
158✔
1715
                ];
119✔
1716
                for ($i = $stackPtr - 1; $i > $startOfScope; $i--) {
158✔
1717
                        if (! in_array($tokens[$i]['code'], $partOfNamespace, true)) {
158✔
1718
                                break;
158✔
1719
                        }
1720
                        $functionName = "{$tokens[$i]['content']}{$functionName}";
8✔
1721
                }
4✔
1722
                return $functionName;
158✔
1723
        }
1724

1725
        /**
1726
         * Return false if the token is definitely not part of a typehint
1727
         *
1728
         * @param File $phpcsFile
1729
         * @param int  $stackPtr
1730
         *
1731
         * @return bool
1732
         */
1733
        private static function isTokenPossiblyPartOfTypehint(File $phpcsFile, $stackPtr)
232✔
1734
        {
1735
                $tokens = $phpcsFile->getTokens();
232✔
1736
                $token = $tokens[$stackPtr];
232✔
1737
                if ($token['code'] === 'PHPCS_T_NULLABLE') {
232✔
1738
                        return true;
8✔
1739
                }
1740
                if ($token['code'] === T_NAME_QUALIFIED) {
232✔
NEW
UNCOV
1741
                        return true;
×
1742
                }
1743
                if ($token['code'] === T_NAME_RELATIVE) {
232✔
NEW
UNCOV
1744
                        return true;
×
1745
                }
1746
                if ($token['code'] === T_NAME_FULLY_QUALIFIED) {
232✔
1747
                        return true;
12✔
1748
                }
1749
                if ($token['code'] === T_NS_SEPARATOR) {
232✔
1750
                        return true;
8✔
1751
                }
1752
                if ($token['code'] === T_STRING) {
232✔
1753
                        return true;
32✔
1754
                }
1755
                if ($token['code'] === T_TRUE) {
232✔
1756
                        return true;
8✔
1757
                }
1758
                if ($token['code'] === T_FALSE) {
232✔
1759
                        return true;
8✔
1760
                }
1761
                if ($token['code'] === T_NULL) {
232✔
1762
                        return true;
8✔
1763
                }
1764
                if ($token['content'] === '|') {
232✔
1765
                        return true;
16✔
1766
                }
1767
                if (in_array($token['code'], Tokens::$emptyTokens)) {
232✔
1768
                        return true;
32✔
1769
                }
1770
                return false;
232✔
1771
        }
1772

1773
        /**
1774
         * Return true if the token is inside a typehint
1775
         *
1776
         * @param File $phpcsFile
1777
         * @param int  $stackPtr
1778
         *
1779
         * @return bool
1780
         */
1781
        public static function isTokenPartOfTypehint(File $phpcsFile, $stackPtr)
232✔
1782
        {
1783
                $tokens = $phpcsFile->getTokens();
232✔
1784

1785
                if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $stackPtr)) {
232✔
1786
                        return false;
228✔
1787
                }
1788

1789
                // Examine every following token, ignoring everything that might be part of
1790
                // a typehint. If we find a variable at the end, this is part of a
1791
                // typehint.
1792
                $i = $stackPtr;
32✔
1793
                while (true) {
32✔
1794
                        $i += 1;
32✔
1795
                        if (! isset($tokens[$i])) {
32✔
UNCOV
1796
                                return false;
×
1797
                        }
1798
                        if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $i)) {
32✔
1799
                                return ($tokens[$i]['code'] === T_VARIABLE);
32✔
1800
                        }
1801
                }
8✔
1802
        }
1803

1804
        /**
1805
         * Return true if the token is inside an abstract class.
1806
         *
1807
         * @param File $phpcsFile
1808
         * @param int  $stackPtr
1809
         *
1810
         * @return bool
1811
         */
1812
        public static function isInAbstractClass(File $phpcsFile, $stackPtr)
108✔
1813
        {
1814
                $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
108✔
1815
                if (! is_int($classIndex)) {
108✔
1816
                        return false;
92✔
1817
                }
1818
                $classProperties = $phpcsFile->getClassProperties($classIndex);
16✔
1819
                return $classProperties['is_abstract'];
16✔
1820
        }
1821

1822
        /**
1823
         * Return true if the function body is empty or contains only `return;`
1824
         *
1825
         * @param File $phpcsFile
1826
         * @param int  $stackPtr  The index of the function keyword.
1827
         *
1828
         * @return bool
1829
         */
1830
        public static function isFunctionBodyEmpty(File $phpcsFile, $stackPtr)
8✔
1831
        {
1832
                $tokens = $phpcsFile->getTokens();
8✔
1833
                if ($tokens[$stackPtr]['code'] !== T_FUNCTION) {
8✔
UNCOV
1834
                        return false;
×
1835
                }
1836
                $functionScopeStart = $tokens[$stackPtr]['scope_opener'];
8✔
1837
                $functionScopeEnd = $tokens[$stackPtr]['scope_closer'];
8✔
1838
                $tokensToIgnore = array_merge(
8✔
1839
                        Tokens::$emptyTokens,
8✔
1840
                        [
4✔
1841
                                T_RETURN,
8✔
1842
                                T_SEMICOLON,
8✔
1843
                                T_OPEN_CURLY_BRACKET,
8✔
1844
                                T_CLOSE_CURLY_BRACKET,
8✔
1845
                        ]
4✔
1846
                );
6✔
1847
                for ($i = $functionScopeStart; $i < $functionScopeEnd; $i++) {
8✔
1848
                        if (! in_array($tokens[$i]['code'], $tokensToIgnore, true)) {
8✔
1849
                                return false;
8✔
1850
                        }
1851
                }
2✔
1852
                return true;
8✔
1853
        }
1854
}
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