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

sirbrillig / phpcs-variable-analysis / 15118106362

19 May 2025 04:19PM UTC coverage: 93.79%. Remained the same
15118106362

Pull #353

github

jrfnl
Ruleset: update schema URL

PHPCS now offers permalinks for the schema.

Refs:
* PHPCSStandards/PHP_CodeSniffer 1094
* https://github.com/PHPCSStandards/schema.phpcodesniffer.com
Pull Request #353: Ruleset: update schema URL

1903 of 2029 relevant lines covered (93.79%)

138.99 hits per line

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

89.41
/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
                );
356✔
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 array<int|string|null>
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
                }
10✔
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
                }
20✔
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
                }
82✔
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
                }
64✔
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
                ];
8✔
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 {
12✔
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
                ];
284✔
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) && $tokens[$functionPtr]['code'] === T_STRING) {
216✔
323
                                return $functionPtr;
162✔
324
                        }
325
                }
73✔
326
                return null;
318✔
327
        }
328

329
        /**
330
         * @param File $phpcsFile
331
         * @param int  $stackPtr
332
         *
333
         * @return array<int, array<int>>
334
         */
335
        public static function findFunctionCallArguments(File $phpcsFile, $stackPtr)
36✔
336
        {
337
                $tokens = $phpcsFile->getTokens();
36✔
338

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

348
                // $stackPtr is the function name, find our brackets after it
349
                $openPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
36✔
350
                if (($openPtr === false) || ($tokens[$openPtr]['code'] !== T_OPEN_PARENTHESIS)) {
36✔
351
                        return [];
×
352
                }
353

354
                if (!isset($tokens[$openPtr]['parenthesis_closer'])) {
36✔
355
                        return [];
×
356
                }
357
                $closePtr = $tokens[$openPtr]['parenthesis_closer'];
36✔
358

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

382
                return $argPtrs;
36✔
383
        }
384

385
        /**
386
         * @param File $phpcsFile
387
         * @param int  $stackPtr
388
         *
389
         * @return ?int
390
         */
391
        public static function getNextAssignPointer(File $phpcsFile, $stackPtr)
344✔
392
        {
393
                $tokens = $phpcsFile->getTokens();
344✔
394

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

407
        /**
408
         * @param string $varName
409
         *
410
         * @return string
411
         */
412
        public static function normalizeVarName($varName)
356✔
413
        {
414
                $result = preg_replace('/[{}$]/', '', $varName);
356✔
415
                return $result ? $result : $varName;
356✔
416
        }
417

418
        /**
419
         * @param File   $phpcsFile
420
         * @param int    $stackPtr
421
         * @param string $varName   (optional) if it differs from the normalized 'content' of the token at $stackPtr
422
         *
423
         * @return ?int
424
         */
425
        public static function findVariableScope(File $phpcsFile, $stackPtr, $varName = null)
356✔
426
        {
427
                $tokens = $phpcsFile->getTokens();
356✔
428
                $token = $tokens[$stackPtr];
356✔
429
                $varName = isset($varName) ? $varName : self::normalizeVarName($token['content']);
356✔
430

431
                $enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
356✔
432

433
                if (!is_null($enclosingScopeIndex)) {
356✔
434
                        $arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
352✔
435
                        $isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex);
352✔
436
                        if ($isTokenInsideArrowFunctionBody) {
352✔
437
                                // Get the list of variables defined by the arrow function
438
                                // If this matches any of them, the scope is the arrow function,
439
                                // otherwise, it uses the enclosing scope.
440
                                if ($arrowFunctionIndex) {
36✔
441
                                        $variableNames = self::getVariablesDefinedByArrowFunction($phpcsFile, $arrowFunctionIndex);
36✔
442
                                        self::debug('findVariableScope: looking for', $varName, 'in arrow function variables', $variableNames);
36✔
443
                                        if (in_array($varName, $variableNames, true)) {
36✔
444
                                                return $arrowFunctionIndex;
36✔
445
                                        }
446
                                }
14✔
447
                        }
14✔
448
                }
176✔
449

450
                return $enclosingScopeIndex;
356✔
451
        }
452

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

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

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

556
                $startOfTokenScope = self::getStartOfTokenScope($phpcsFile, $stackPtr);
356✔
557
                if (is_int($startOfTokenScope) && $startOfTokenScope > 0) {
356✔
558
                        return $startOfTokenScope;
336✔
559
                }
560

561
                // If there is no "conditions" array, this is a function definition argument.
562
                if (self::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
292✔
563
                        $functionPtr = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
232✔
564
                        if (! is_int($functionPtr)) {
232✔
565
                                throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
566
                        }
567
                        return $functionPtr;
232✔
568
                }
569

570
                self::debug('Cannot find function scope for variable at', $stackPtr);
124✔
571
                return $startOfTokenScope;
124✔
572
        }
573

574
        /**
575
         * Return the token index of the scope start for a variable token
576
         *
577
         * This will only work for a variable within a function's body. Otherwise,
578
         * see `findVariableScope`, which is more complex.
579
         *
580
         * Note that if used on a variable in an arrow function, it will return the
581
         * enclosing function's scope, which may be incorrect.
582
         *
583
         * @param File $phpcsFile
584
         * @param int  $stackPtr
585
         *
586
         * @return ?int
587
         */
588
        private static function getStartOfTokenScope(File $phpcsFile, $stackPtr)
356✔
589
        {
590
                $tokens = $phpcsFile->getTokens();
356✔
591
                $token = $tokens[$stackPtr];
356✔
592

593
                $inClass = false;
356✔
594
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
356✔
595
                $functionTokenTypes = [
178✔
596
                        T_FUNCTION,
356✔
597
                        T_CLOSURE,
356✔
598
                ];
356✔
599
                foreach (array_reverse($conditions, true) as $scopePtr => $scopeCode) {
356✔
600
                        if (in_array($scopeCode, $functionTokenTypes, true) || self::isArrowFunction($phpcsFile, $scopePtr)) {
340✔
601
                                return $scopePtr;
336✔
602
                        }
603
                        if (isset(Tokens::$ooScopeTokens[$scopeCode]) === true) {
164✔
604
                                $inClass = true;
68✔
605
                        }
34✔
606
                }
164✔
607

608
                if ($inClass) {
292✔
609
                        // If this is inside a class and not inside a function, this is either a
610
                        // class member variable definition, or a function argument. If it is a
611
                        // variable definition, it has no scope on its own (it can only be used
612
                        // with an object reference). If it is a function argument, we need to do
613
                        // more work (see `findVariableScopeExceptArrowFunctions`).
614
                        return null;
60✔
615
                }
616

617
                // If we can't find a scope, let's use the first token of the file.
618
                return 0;
252✔
619
        }
620

621
        /**
622
         * @param File $phpcsFile
623
         * @param int  $stackPtr
624
         *
625
         * @return bool
626
         */
627
        public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $stackPtr)
×
628
        {
629
                $tokens = $phpcsFile->getTokens();
×
630
                $token = $tokens[$stackPtr];
×
631
                $openParenIndices = isset($token['nested_parenthesis']) ? $token['nested_parenthesis'] : [];
×
632
                if (empty($openParenIndices)) {
×
633
                        return false;
×
634
                }
635
                $openParenPtr = $openParenIndices[0];
×
636
                return self::isArrowFunction($phpcsFile, $openParenPtr - 1);
×
637
        }
638

639
        /**
640
         * @param File $phpcsFile
641
         * @param int  $stackPtr
642
         * @param int  $enclosingScopeIndex
643
         *
644
         * @return ?int
645
         */
646
        public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
352✔
647
        {
648
                $arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
352✔
649
                if (! is_int($arrowFunctionIndex)) {
352✔
650
                        return null;
352✔
651
                }
652
                $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex);
36✔
653
                if (! $arrowFunctionInfo) {
36✔
654
                        return null;
×
655
                }
656

657
                // We found the closest arrow function before this token. If the token is
658
                // within the scope of that arrow function, then return it.
659
                if ($stackPtr > $arrowFunctionInfo['scope_opener'] && $stackPtr < $arrowFunctionInfo['scope_closer']) {
36✔
660
                        return $arrowFunctionIndex;
36✔
661
                }
662

663
                // If the token is after the scope of the closest arrow function, we may
664
                // still be inside the scope of a nested arrow function, so we need to
665
                // search further back until we are certain there are no more arrow
666
                // functions.
667
                if ($stackPtr > $arrowFunctionInfo['scope_closer']) {
36✔
668
                        return self::getContainingArrowFunctionIndex($phpcsFile, $arrowFunctionIndex, $enclosingScopeIndex);
36✔
669
                }
670

671
                return null;
×
672
        }
673

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

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

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

774
                // Find the scope closer
775
                $scopeCloserIndex = null;
36✔
776
                $foundCurlyPairs = 0;
36✔
777
                $foundArrayPairs = 0;
36✔
778
                $foundParenPairs = 0;
36✔
779
                $arrowBodyStart = $tokens[$stackPtr]['parenthesis_closer'] + 1;
36✔
780
                $lastToken = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
36✔
781
                for ($index = $arrowBodyStart; $index < $lastToken; $index++) {
36✔
782
                        $token = $tokens[$index];
36✔
783
                        if (empty($token['code'])) {
36✔
784
                                $scopeCloserIndex = $index;
×
785
                                break;
×
786
                        }
787

788
                        $code = $token['code'];
36✔
789

790
                        // A semicolon is always a closer.
791
                        if ($code === T_SEMICOLON) {
36✔
792
                                $scopeCloserIndex = $index;
28✔
793
                                break;
28✔
794
                        }
795

796
                        // Track pair opening tokens.
797
                        if ($code === T_OPEN_CURLY_BRACKET) {
36✔
798
                                $foundCurlyPairs += 1;
8✔
799
                                continue;
8✔
800
                        }
801
                        if ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) {
36✔
802
                                $foundArrayPairs += 1;
28✔
803
                                continue;
28✔
804
                        }
805
                        if ($code === T_OPEN_PARENTHESIS) {
36✔
806
                                $foundParenPairs += 1;
28✔
807
                                continue;
28✔
808
                        }
809

810
                        // A pair closing is only an arrow func closer if there was no matching opening token.
811
                        if ($code === T_CLOSE_CURLY_BRACKET) {
36✔
812
                                if ($foundCurlyPairs === 0) {
×
813
                                        $scopeCloserIndex = $index;
×
814
                                        break;
×
815
                                }
816
                                $foundCurlyPairs -= 1;
×
817
                                continue;
×
818
                        }
819
                        if ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) {
36✔
820
                                if ($foundArrayPairs === 0) {
28✔
821
                                        $scopeCloserIndex = $index;
×
822
                                        break;
×
823
                                }
824
                                $foundArrayPairs -= 1;
28✔
825
                                continue;
28✔
826
                        }
827
                        if ($code === T_CLOSE_PARENTHESIS) {
36✔
828
                                if ($foundParenPairs === 0) {
28✔
829
                                        $scopeCloserIndex = $index;
8✔
830
                                        break;
8✔
831
                                }
832
                                $foundParenPairs -= 1;
28✔
833
                                continue;
28✔
834
                        }
835

836
                        // A comma is a closer only if we are not inside an opening token.
837
                        if ($code === T_COMMA) {
36✔
838
                                if (empty($foundArrayPairs) && empty($foundParenPairs) && empty($foundCurlyPairs)) {
28✔
839
                                        $scopeCloserIndex = $index;
16✔
840
                                        break;
16✔
841
                                }
842
                                continue;
20✔
843
                        }
844
                }
18✔
845

846
                if (! is_int($scopeCloserIndex)) {
36✔
847
                        return null;
×
848
                }
849

850
                return [
18✔
851
                        'scope_opener' => $stackPtr,
36✔
852
                        'scope_closer' => $scopeCloserIndex,
36✔
853
                ];
36✔
854
        }
855

856
        /**
857
         * Determine if a token is a list opener for list assignment/destructuring.
858
         *
859
         * The index provided can be either the opening square brace of a short list
860
         * assignment like the first character of `[$a] = $b;` or the `list` token of
861
         * an expression like `list($a) = $b;` or the opening parenthesis of that
862
         * expression.
863
         *
864
         * @param File $phpcsFile
865
         * @param int  $listOpenerIndex
866
         *
867
         * @return bool
868
         */
869
        private static function isListAssignment(File $phpcsFile, $listOpenerIndex)
32✔
870
        {
871
                $tokens = $phpcsFile->getTokens();
32✔
872
                // Match `[$a] = $b;` except for when the previous token is a parenthesis.
873
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SHORT_ARRAY) {
32✔
874
                        return true;
28✔
875
                }
876
                // Match `list($a) = $b;`
877
                if ($tokens[$listOpenerIndex]['code'] === T_LIST) {
32✔
878
                        return true;
32✔
879
                }
880

881
                // If $listOpenerIndex is the open parenthesis of `list($a) = $b;`, then
882
                // match that too.
883
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_PARENTHESIS) {
16✔
884
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
4✔
885
                        if (
886
                                isset($tokens[$previousTokenPtr])
4✔
887
                                && $tokens[$previousTokenPtr]['code'] === T_LIST
4✔
888
                        ) {
2✔
889
                                return true;
4✔
890
                        }
891
                        return true;
×
892
                }
893

894
                // If the list opener token is a square bracket that is preceeded by a
895
                // close parenthesis that has an owner which is a scope opener, then this
896
                // is a list assignment and not an array access.
897
                //
898
                // Match `if (true) [$a] = $b;`
899
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SQUARE_BRACKET) {
12✔
900
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
12✔
901
                        if (
902
                                isset($tokens[$previousTokenPtr])
12✔
903
                                && $tokens[$previousTokenPtr]['code'] === T_CLOSE_PARENTHESIS
12✔
904
                                && isset($tokens[$previousTokenPtr]['parenthesis_owner'])
12✔
905
                                && isset(Tokens::$scopeOpeners[$tokens[$tokens[$previousTokenPtr]['parenthesis_owner']]['code']])
12✔
906
                        ) {
6✔
907
                                return true;
4✔
908
                        }
909
                }
4✔
910

911
                return false;
8✔
912
        }
913

914
        /**
915
         * Return a list of indices for variables assigned within a list assignment.
916
         *
917
         * The index provided can be either the opening square brace of a short list
918
         * assignment like the first character of `[$a] = $b;` or the `list` token of
919
         * an expression like `list($a) = $b;` or the opening parenthesis of that
920
         * expression.
921
         *
922
         * @param File $phpcsFile
923
         * @param int  $listOpenerIndex
924
         *
925
         * @return ?array<int>
926
         */
927
        public static function getListAssignments(File $phpcsFile, $listOpenerIndex)
48✔
928
        {
929
                $tokens = $phpcsFile->getTokens();
48✔
930
                self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]);
48✔
931

932
                // First find the end of the list
933
                $closePtr = null;
48✔
934
                if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) {
48✔
935
                        $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer'];
48✔
936
                }
24✔
937
                if (isset($tokens[$listOpenerIndex]['bracket_closer'])) {
48✔
938
                        $closePtr = $tokens[$listOpenerIndex]['bracket_closer'];
48✔
939
                }
24✔
940
                if (! $closePtr) {
48✔
941
                        return null;
×
942
                }
943

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

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

971
                        // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion)
972
                        $isNestedAssignment = null;
28✔
973
                        $parentListOpener = array_keys(array_reverse($parents, true))[0];
28✔
974
                        $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener);
28✔
975
                        if ($isNestedAssignment === null) {
28✔
976
                                return null;
28✔
977
                        }
978
                }
2✔
979

980
                $variablePtrs = [];
32✔
981

982
                $currentPtr = $listOpenerIndex;
32✔
983
                $variablePtr = 0;
32✔
984
                while ($currentPtr < $closePtr && is_int($variablePtr)) {
32✔
985
                        $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr);
32✔
986
                        if (is_int($variablePtr)) {
32✔
987
                                $variablePtrs[] = $variablePtr;
32✔
988
                        }
16✔
989
                        ++$currentPtr;
32✔
990
                }
16✔
991

992
                if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) {
32✔
993
                        return null;
8✔
994
                }
995

996
                return $variablePtrs;
32✔
997
        }
998

999
        /**
1000
         * @param File $phpcsFile
1001
         * @param int  $stackPtr
1002
         *
1003
         * @return string[]
1004
         */
1005
        public static function getVariablesDefinedByArrowFunction(File $phpcsFile, $stackPtr)
36✔
1006
        {
1007
                $tokens = $phpcsFile->getTokens();
36✔
1008
                $arrowFunctionToken = $tokens[$stackPtr];
36✔
1009
                $variableNames = [];
36✔
1010
                self::debug('looking for variables in arrow function token', $arrowFunctionToken);
36✔
1011
                for ($index = $arrowFunctionToken['parenthesis_opener']; $index < $arrowFunctionToken['parenthesis_closer']; $index++) {
36✔
1012
                        $token = $tokens[$index];
36✔
1013
                        if ($token['code'] === T_VARIABLE) {
36✔
1014
                                $variableNames[] = self::normalizeVarName($token['content']);
36✔
1015
                        }
18✔
1016
                }
18✔
1017
                self::debug('found these variables in arrow function token', $variableNames);
36✔
1018
                return $variableNames;
36✔
1019
        }
1020

1021
        /**
1022
         * @return void
1023
         */
1024
        public static function debug()
356✔
1025
        {
1026
                $messages = func_get_args();
356✔
1027
                if (! defined('PHP_CODESNIFFER_VERBOSITY')) {
356✔
1028
                        return;
×
1029
                }
1030
                if (PHP_CODESNIFFER_VERBOSITY <= 3) {
356✔
1031
                        return;
356✔
1032
                }
1033
                $output = PHP_EOL . 'VariableAnalysisSniff: DEBUG:';
×
1034
                foreach ($messages as $message) {
×
1035
                        if (is_string($message) || is_numeric($message)) {
×
1036
                                $output .= ' "' . $message . '"';
×
1037
                                continue;
×
1038
                        }
1039
                        $output .= PHP_EOL . var_export($message, true) . PHP_EOL;
×
1040
                }
1041
                $output .= PHP_EOL;
×
1042
                echo $output;
×
1043
        }
1044

1045
        /**
1046
         * @param string $pattern
1047
         * @param string $value
1048
         *
1049
         * @return string[]
1050
         */
1051
        public static function splitStringToArray($pattern, $value)
28✔
1052
        {
1053
                if (empty($pattern)) {
28✔
1054
                        return [];
×
1055
                }
1056
                $result = preg_split($pattern, $value);
28✔
1057
                return is_array($result) ? $result : [];
28✔
1058
        }
1059

1060
        /**
1061
         * @param string $varName
1062
         *
1063
         * @return bool
1064
         */
1065
        public static function isVariableANumericVariable($varName)
340✔
1066
        {
1067
                return is_numeric(substr($varName, 0, 1));
340✔
1068
        }
1069

1070
        /**
1071
         * @param File $phpcsFile
1072
         * @param int  $stackPtr
1073
         *
1074
         * @return bool
1075
         */
1076
        public static function isVariableInsideElseCondition(File $phpcsFile, $stackPtr)
332✔
1077
        {
1078
                $tokens = $phpcsFile->getTokens();
332✔
1079
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
332✔
1080
                $nonFunctionTokenTypes[] = T_OPEN_PARENTHESIS;
332✔
1081
                $nonFunctionTokenTypes[] = T_INLINE_HTML;
332✔
1082
                $nonFunctionTokenTypes[] = T_CLOSE_TAG;
332✔
1083
                $nonFunctionTokenTypes[] = T_VARIABLE;
332✔
1084
                $nonFunctionTokenTypes[] = T_ELLIPSIS;
332✔
1085
                $nonFunctionTokenTypes[] = T_COMMA;
332✔
1086
                $nonFunctionTokenTypes[] = T_STRING;
332✔
1087
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
332✔
1088
                $elsePtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $stackPtr - 1, null, true, null, true));
332✔
1089
                $elseTokenTypes = [
166✔
1090
                        T_ELSE,
332✔
1091
                        T_ELSEIF,
332✔
1092
                ];
332✔
1093
                if (is_int($elsePtr) && in_array($tokens[$elsePtr]['code'], $elseTokenTypes, true)) {
332✔
1094
                        return true;
16✔
1095
                }
1096
                return false;
332✔
1097
        }
1098

1099
        /**
1100
         * @param File $phpcsFile
1101
         * @param int  $stackPtr
1102
         *
1103
         * @return bool
1104
         */
1105
        public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr)
332✔
1106
        {
1107
                $tokens = $phpcsFile->getTokens();
332✔
1108
                $token = $tokens[$stackPtr];
332✔
1109
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
332✔
1110
                $elseTokenTypes = [
166✔
1111
                        T_ELSE,
332✔
1112
                        T_ELSEIF,
332✔
1113
                ];
332✔
1114
                foreach (array_reverse($conditions, true) as $scopeCode) {
332✔
1115
                        if (in_array($scopeCode, $elseTokenTypes, true)) {
312✔
1116
                                return true;
16✔
1117
                        }
1118
                }
166✔
1119

1120
                // Some else body code will not have conditions because it is inline (no
1121
                // curly braces) so we have to look in other ways.
1122
                $previousSemicolonPtr = $phpcsFile->findPrevious([T_SEMICOLON], $stackPtr - 1);
332✔
1123
                if (! is_int($previousSemicolonPtr)) {
332✔
1124
                        $previousSemicolonPtr = 0;
152✔
1125
                }
76✔
1126
                $elsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1, $previousSemicolonPtr);
332✔
1127
                if (is_int($elsePtr)) {
332✔
1128
                        return true;
8✔
1129
                }
1130

1131
                return false;
332✔
1132
        }
1133

1134
        /**
1135
         * @param File $phpcsFile
1136
         * @param int  $stackPtr
1137
         *
1138
         * @return int[]
1139
         */
1140
        public static function getAttachedBlockIndicesForElse(File $phpcsFile, $stackPtr)
24✔
1141
        {
1142
                $currentElsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1);
24✔
1143
                if (! is_int($currentElsePtr)) {
24✔
1144
                        throw new \Exception("Cannot find expected else at {$stackPtr}");
×
1145
                }
1146

1147
                $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
24✔
1148
                if (! is_int($ifPtr)) {
24✔
1149
                        throw new \Exception("Cannot find if for else at {$stackPtr}");
×
1150
                }
1151
                $blockIndices = [$ifPtr];
24✔
1152

1153
                $previousElseIfPtr = $currentElsePtr;
24✔
1154
                do {
1155
                        $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
24✔
1156
                        if (is_int($elseIfPtr)) {
24✔
1157
                                $blockIndices[] = $elseIfPtr;
16✔
1158
                                $previousElseIfPtr = $elseIfPtr;
16✔
1159
                        }
8✔
1160
                } while (is_int($elseIfPtr));
24✔
1161

1162
                return $blockIndices;
24✔
1163
        }
1164

1165
        /**
1166
         * @param int $needle
1167
         * @param int $scopeStart
1168
         * @param int $scopeEnd
1169
         *
1170
         * @return bool
1171
         */
1172
        public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
24✔
1173
        {
1174
                return ($needle > $scopeStart && $needle < $scopeEnd);
24✔
1175
        }
1176

1177
        /**
1178
         * @param File $phpcsFile
1179
         * @param int  $scopeStartIndex
1180
         *
1181
         * @return int
1182
         */
1183
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
356✔
1184
        {
1185
                $tokens = $phpcsFile->getTokens();
356✔
1186
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
356✔
1187

1188
                if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) {
356✔
1189
                        $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex);
36✔
1190
                        $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex;
36✔
1191
                }
18✔
1192

1193
                if ($scopeStartIndex === 0) {
356✔
1194
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
356✔
1195
                }
178✔
1196
                return $scopeCloserIndex;
356✔
1197
        }
1198

1199
        /**
1200
         * @param File $phpcsFile
1201
         *
1202
         * @return int
1203
         */
1204
        public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
356✔
1205
        {
1206
                $tokens = $phpcsFile->getTokens();
356✔
1207
                foreach (array_reverse($tokens, true) as $index => $token) {
356✔
1208
                        if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
356✔
1209
                                return $index;
356✔
1210
                        }
1211
                }
174✔
1212
                self::debug('no non-empty token found for end of file');
×
1213
                return 0;
×
1214
        }
1215

1216
        /**
1217
         * @param VariableInfo $varInfo
1218
         * @param ScopeInfo    $scopeInfo
1219
         *
1220
         * @return bool
1221
         */
1222
        public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo)
68✔
1223
        {
1224
                $foundVarPosition = false;
68✔
1225
                foreach ($scopeInfo->variables as $variable) {
68✔
1226
                        if ($variable === $varInfo) {
68✔
1227
                                $foundVarPosition = true;
68✔
1228
                                continue;
68✔
1229
                        }
1230
                        if (! $foundVarPosition) {
44✔
1231
                                continue;
36✔
1232
                        }
1233
                        if ($variable->scopeType !== ScopeType::PARAM) {
44✔
1234
                                continue;
36✔
1235
                        }
1236
                        if ($variable->firstRead) {
16✔
1237
                                return true;
16✔
1238
                        }
1239
                }
34✔
1240
                return false;
68✔
1241
        }
1242

1243
        /**
1244
         * @param File         $phpcsFile
1245
         * @param VariableInfo $varInfo
1246
         * @param ScopeInfo    $scopeInfo
1247
         *
1248
         * @return bool
1249
         */
1250
        public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
4✔
1251
        {
1252
                $requireTokens = [
2✔
1253
                        T_REQUIRE,
4✔
1254
                        T_REQUIRE_ONCE,
4✔
1255
                        T_INCLUDE,
4✔
1256
                        T_INCLUDE_ONCE,
4✔
1257
                ];
4✔
1258
                $indexToStartSearch = $varInfo->firstDeclared;
4✔
1259
                if (! empty($varInfo->firstInitialized)) {
4✔
1260
                        $indexToStartSearch = $varInfo->firstInitialized;
4✔
1261
                }
2✔
1262
                $tokens = $phpcsFile->getTokens();
4✔
1263
                $indexToStopSearch = isset($tokens[$scopeInfo->scopeStartIndex]['scope_closer']) ? $tokens[$scopeInfo->scopeStartIndex]['scope_closer'] : null;
4✔
1264
                if (! is_int($indexToStartSearch) || ! is_int($indexToStopSearch)) {
4✔
1265
                        return false;
×
1266
                }
1267
                $requireTokenIndex = $phpcsFile->findNext($requireTokens, $indexToStartSearch + 1, $indexToStopSearch);
4✔
1268
                if (is_int($requireTokenIndex)) {
4✔
1269
                        return true;
4✔
1270
                }
1271
                return false;
×
1272
        }
1273

1274
        /**
1275
         * Find the index of the function keyword for a token in a function call's arguments
1276
         *
1277
         * For the variable `$foo` in the expression `doSomething($foo)`, this will
1278
         * return the index of the `doSomething` token.
1279
         *
1280
         * @param File $phpcsFile
1281
         * @param int  $stackPtr
1282
         *
1283
         * @return ?int
1284
         */
1285
        public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr)
344✔
1286
        {
1287
                $tokens = $phpcsFile->getTokens();
344✔
1288
                $token = $tokens[$stackPtr];
344✔
1289
                if (empty($token['nested_parenthesis'])) {
344✔
1290
                        return null;
336✔
1291
                }
1292
                /**
1293
                 * @var array<int|string|null>
1294
                 */
1295
                $startingParenthesis = array_keys($token['nested_parenthesis']);
216✔
1296
                $startOfArguments = end($startingParenthesis);
216✔
1297
                if (! is_int($startOfArguments)) {
216✔
1298
                        return null;
×
1299
                }
1300

1301
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
216✔
1302
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
216✔
1303
                if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
216✔
1304
                        return null;
×
1305
                }
1306
                if (
1307
                        $tokens[$functionPtr]['content'] === 'function'
216✔
1308
                        || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))
216✔
1309
                ) {
108✔
1310
                        // If there is a function/fn keyword before the beginning of the parens,
1311
                        // this is a function definition and not a function call.
1312
                        return null;
×
1313
                }
1314
                if (! empty($tokens[$functionPtr]['scope_opener'])) {
216✔
1315
                        // If the alleged function name has a scope, this is not a function call.
1316
                        return null;
142✔
1317
                }
1318

1319
                $functionNameType = $tokens[$functionPtr]['code'];
182✔
1320
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
182✔
1321
                        // If the alleged function name is not a variable or a string, this is
1322
                        // not a function call.
1323
                        return null;
48✔
1324
                }
1325

1326
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
178✔
1327
                        // If the variable is inside a different scope than the function name,
1328
                        // the function call doesn't apply to the variable.
1329
                        return null;
32✔
1330
                }
1331

1332
                return $functionPtr;
178✔
1333
        }
1334

1335
        /**
1336
         * @param File $phpcsFile
1337
         * @param int  $stackPtr
1338
         *
1339
         * @return bool
1340
         */
1341
        public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
332✔
1342
        {
1343
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
332✔
1344
                if (! is_int($functionIndex)) {
332✔
1345
                        return false;
314✔
1346
                }
1347
                $tokens = $phpcsFile->getTokens();
178✔
1348
                if (! isset($tokens[$functionIndex])) {
178✔
1349
                        return false;
×
1350
                }
1351
                $allowedFunctionNames = [
89✔
1352
                        'isset',
178✔
1353
                        'empty',
178✔
1354
                ];
178✔
1355
                if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) {
178✔
1356
                        return true;
20✔
1357
                }
1358
                return false;
170✔
1359
        }
1360

1361
        /**
1362
         * @param File $phpcsFile
1363
         * @param int  $stackPtr
1364
         *
1365
         * @return bool
1366
         */
1367
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
260✔
1368
        {
1369
                $tokens = $phpcsFile->getTokens();
260✔
1370
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
260✔
1371

1372
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
260✔
1373
                if (! is_int($arrayPushOperatorIndex1)) {
260✔
1374
                        return false;
×
1375
                }
1376
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
260✔
1377
                        return false;
260✔
1378
                }
1379

1380
                $arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true));
36✔
1381
                if (! is_int($arrayPushOperatorIndex2)) {
36✔
1382
                        return false;
×
1383
                }
1384
                if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') {
36✔
1385
                        return false;
8✔
1386
                }
1387

1388
                $arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true));
28✔
1389
                if (! is_int($arrayPushOperatorIndex3)) {
28✔
1390
                        return false;
×
1391
                }
1392
                if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') {
28✔
1393
                        return false;
×
1394
                }
1395

1396
                return true;
28✔
1397
        }
1398

1399
        /**
1400
         * @param File $phpcsFile
1401
         * @param int  $stackPtr
1402
         *
1403
         * @return bool
1404
         */
1405
        public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
260✔
1406
        {
1407
                $functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
260✔
1408
                if (! is_int($functionIndex)) {
260✔
1409
                        return false;
230✔
1410
                }
1411
                $tokens = $phpcsFile->getTokens();
102✔
1412
                if (! isset($tokens[$functionIndex])) {
102✔
1413
                        return false;
×
1414
                }
1415
                if ($tokens[$functionIndex]['content'] === 'unset') {
102✔
1416
                        return true;
8✔
1417
                }
1418
                return false;
94✔
1419
        }
1420

1421
        /**
1422
         * @param File $phpcsFile
1423
         * @param int  $stackPtr
1424
         *
1425
         * @return bool
1426
         */
1427
        public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr)
324✔
1428
        {
1429
                $previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
324✔
1430
                if (! is_int($previousStatementPtr)) {
324✔
1431
                        $previousStatementPtr = 1;
40✔
1432
                }
20✔
1433
                $previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
324✔
1434
                if (is_int($previousTokenPtr)) {
324✔
1435
                        return true;
4✔
1436
                }
1437
                return false;
324✔
1438
        }
1439

1440
        /**
1441
         * @param File $phpcsFile
1442
         * @param int  $stackPtr
1443
         *
1444
         * @return bool
1445
         */
1446
        public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
344✔
1447
        {
1448
                // Is the next non-whitespace an assignment?
1449
                $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
344✔
1450
                if (! is_int($assignPtr)) {
344✔
1451
                        return false;
336✔
1452
                }
1453

1454
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1455
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
324✔
1456
                        self::debug('found variable variable');
4✔
1457
                        return false;
4✔
1458
                }
1459
                return true;
324✔
1460
        }
1461

1462
        /**
1463
         * @param File $phpcsFile
1464
         * @param int  $stackPtr
1465
         *
1466
         * @return bool
1467
         */
1468
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
324✔
1469
        {
1470
                $tokens = $phpcsFile->getTokens();
324✔
1471

1472
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
324✔
1473
                if ($prev === false) {
324✔
1474
                        return false;
×
1475
                }
1476
                if ($tokens[$prev]['code'] === T_DOLLAR) {
324✔
1477
                        return true;
4✔
1478
                }
1479
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
324✔
1480
                        return false;
268✔
1481
                }
1482

1483
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
232✔
1484
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
232✔
1485
                        return true;
×
1486
                }
1487
                return false;
232✔
1488
        }
1489

1490
        /**
1491
         * @param File $phpcsFile
1492
         * @param int  $stackPtr
1493
         *
1494
         * @return EnumInfo|null
1495
         */
1496
        public static function makeEnumInfo(File $phpcsFile, $stackPtr)
4✔
1497
        {
1498
                $tokens = $phpcsFile->getTokens();
4✔
1499
                $token = $tokens[$stackPtr];
4✔
1500

1501
                if (isset($token['scope_opener'])) {
4✔
1502
                        $blockStart = $token['scope_opener'];
2✔
1503
                        $blockEnd = $token['scope_closer'];
2✔
1504
                } else {
1✔
1505
                        // Enums before phpcs could detect them do not have scopes so we have to
1506
                        // find them ourselves.
1507

1508
                        $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
4✔
1509
                        if (! is_int($blockStart)) {
4✔
1510
                                return null;
4✔
1511
                        }
1512
                        $blockEnd = $tokens[$blockStart]['bracket_closer'];
2✔
1513
                }
1514

1515
                return new EnumInfo(
4✔
1516
                        $stackPtr,
4✔
1517
                        $blockStart,
4✔
1518
                        $blockEnd
2✔
1519
                );
4✔
1520
        }
1521

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

1554
                $semicolonCount = 0;
8✔
1555
                $forLoopLevel = $tokens[$forIndex]['level'];
8✔
1556
                $forLoopNestedParensCount = 1;
8✔
1557

1558
                if (isset($tokens[$forIndex]['nested_parenthesis'])) {
8✔
1559
                        $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
×
1560
                }
1561

1562
                for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
8✔
1563
                        if ($tokens[$i]['code'] !== T_SEMICOLON) {
8✔
1564
                                continue;
8✔
1565
                        }
1566

1567
                        if ($tokens[$i]['level'] !== $forLoopLevel) {
8✔
1568
                                continue;
8✔
1569
                        }
1570

1571
                        if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
8✔
1572
                                continue;
×
1573
                        }
1574

1575
                        switch ($semicolonCount) {
1576
                                case 0:
8✔
1577
                                        $initEnd = $i;
8✔
1578
                                        $conditionStart = $initEnd + 1;
8✔
1579
                                        break;
8✔
1580
                                case 1:
8✔
1581
                                        $conditionEnd = $i;
8✔
1582
                                        $incrementStart = $conditionEnd + 1;
8✔
1583
                                        break;
8✔
1584
                        }
1585
                        $semicolonCount += 1;
8✔
1586
                }
4✔
1587

1588
                if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
8✔
1589
                        throw new \Exception("Cannot parse for loop at position {$forIndex}");
×
1590
                }
1591

1592
                return new ForLoopInfo(
8✔
1593
                        $forIndex,
8✔
1594
                        $blockStart,
8✔
1595
                        $blockEnd,
8✔
1596
                        $initStart,
8✔
1597
                        $initEnd,
8✔
1598
                        $conditionStart,
8✔
1599
                        $conditionEnd,
8✔
1600
                        $incrementStart,
8✔
1601
                        $incrementEnd
4✔
1602
                );
8✔
1603
        }
1604

1605
        /**
1606
         * @param int                     $stackPtr
1607
         * @param array<int, ForLoopInfo> $forLoops
1608
         * @return ForLoopInfo|null
1609
         */
1610
        public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
352✔
1611
        {
1612
                foreach ($forLoops as $forLoop) {
352✔
1613
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1614
                                return $forLoop;
8✔
1615
                        }
1616
                }
176✔
1617
                return null;
352✔
1618
        }
1619

1620
        /**
1621
         * Return true if the token looks like constructor promotion.
1622
         *
1623
         * Call on a parameter variable token only.
1624
         *
1625
         * @param File $phpcsFile
1626
         * @param int  $stackPtr
1627
         *
1628
         * @return bool
1629
         */
1630
        public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
232✔
1631
        {
1632
                // If we are not in a function's parameters, this is not promotion.
1633
                $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
232✔
1634
                if (! $functionIndex) {
232✔
1635
                        return false;
×
1636
                }
1637

1638
                $tokens = $phpcsFile->getTokens();
232✔
1639

1640
                // Move backwards from the token, ignoring whitespace, typehints, and the
1641
                // 'readonly' keyword, and return true if the previous token is a
1642
                // visibility keyword (eg: `public`).
1643
                for ($i = $stackPtr - 1; $i > $functionIndex; $i--) {
232✔
1644
                        if (in_array($tokens[$i]['code'], Tokens::$scopeModifiers, true)) {
232✔
1645
                                return true;
12✔
1646
                        }
1647
                        if (in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) {
232✔
1648
                                continue;
160✔
1649
                        }
1650
                        if ($tokens[$i]['content'] === 'readonly') {
232✔
1651
                                continue;
8✔
1652
                        }
1653
                        if (self::isTokenPartOfTypehint($phpcsFile, $i)) {
232✔
1654
                                continue;
32✔
1655
                        }
1656
                        return false;
228✔
1657
                }
1658
                return false;
×
1659
        }
1660

1661
        /**
1662
         * If looking at a function call token, return a string for the full function
1663
         * name including any inline namespace.
1664
         *
1665
         * So for example, if the call looks like `\My\Namespace\doSomething($bar)`
1666
         * and `$stackPtr` refers to `doSomething`, this will return
1667
         * `\My\Namespace\doSomething`.
1668
         *
1669
         * @param File $phpcsFile
1670
         * @param int  $stackPtr
1671
         *
1672
         * @return string|null
1673
         */
1674
        public static function getFunctionNameWithNamespace(File $phpcsFile, $stackPtr)
158✔
1675
        {
1676
                $tokens = $phpcsFile->getTokens();
158✔
1677

1678
                if (! isset($tokens[$stackPtr])) {
158✔
1679
                        return null;
×
1680
                }
1681
                $startOfScope = self::findVariableScope($phpcsFile, $stackPtr);
158✔
1682
                $functionName = $tokens[$stackPtr]['content'];
158✔
1683

1684
                // Move backwards from the token, collecting namespace separators and
1685
                // strings, until we encounter whitespace or something else.
1686
                $partOfNamespace = [T_NS_SEPARATOR, T_STRING];
158✔
1687
                for ($i = $stackPtr - 1; $i > $startOfScope; $i--) {
158✔
1688
                        if (! in_array($tokens[$i]['code'], $partOfNamespace, true)) {
158✔
1689
                                break;
158✔
1690
                        }
1691
                        $functionName = "{$tokens[$i]['content']}{$functionName}";
16✔
1692
                }
8✔
1693
                return $functionName;
158✔
1694
        }
1695

1696
        /**
1697
         * Return false if the token is definitely not part of a typehint
1698
         *
1699
         * @param File $phpcsFile
1700
         * @param int  $stackPtr
1701
         *
1702
         * @return bool
1703
         */
1704
        private static function isTokenPossiblyPartOfTypehint(File $phpcsFile, $stackPtr)
232✔
1705
        {
1706
                $tokens = $phpcsFile->getTokens();
232✔
1707
                $token = $tokens[$stackPtr];
232✔
1708
                if ($token['code'] === 'PHPCS_T_NULLABLE') {
232✔
1709
                        return true;
8✔
1710
                }
1711
                if ($token['code'] === T_NS_SEPARATOR) {
232✔
1712
                        return true;
20✔
1713
                }
1714
                if ($token['code'] === T_STRING) {
232✔
1715
                        return true;
32✔
1716
                }
1717
                if ($token['code'] === T_TRUE) {
232✔
1718
                        return true;
8✔
1719
                }
1720
                if ($token['code'] === T_FALSE) {
232✔
1721
                        return true;
8✔
1722
                }
1723
                if ($token['code'] === T_NULL) {
232✔
1724
                        return true;
8✔
1725
                }
1726
                if ($token['content'] === '|') {
232✔
1727
                        return true;
16✔
1728
                }
1729
                if (in_array($token['code'], Tokens::$emptyTokens)) {
232✔
1730
                        return true;
32✔
1731
                }
1732
                return false;
232✔
1733
        }
1734

1735
        /**
1736
         * Return true if the token is inside a typehint
1737
         *
1738
         * @param File $phpcsFile
1739
         * @param int  $stackPtr
1740
         *
1741
         * @return bool
1742
         */
1743
        public static function isTokenPartOfTypehint(File $phpcsFile, $stackPtr)
232✔
1744
        {
1745
                $tokens = $phpcsFile->getTokens();
232✔
1746

1747
                if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $stackPtr)) {
232✔
1748
                        return false;
228✔
1749
                }
1750

1751
                // Examine every following token, ignoring everything that might be part of
1752
                // a typehint. If we find a variable at the end, this is part of a
1753
                // typehint.
1754
                $i = $stackPtr;
32✔
1755
                while (true) {
32✔
1756
                        $i += 1;
32✔
1757
                        if (! isset($tokens[$i])) {
32✔
1758
                                return false;
×
1759
                        }
1760
                        if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $i)) {
32✔
1761
                                return ($tokens[$i]['code'] === T_VARIABLE);
32✔
1762
                        }
1763
                }
16✔
1764
        }
1765

1766
        /**
1767
         * Return true if the token is inside an abstract class.
1768
         *
1769
         * @param File $phpcsFile
1770
         * @param int  $stackPtr
1771
         *
1772
         * @return bool
1773
         */
1774
        public static function isInAbstractClass(File $phpcsFile, $stackPtr)
108✔
1775
        {
1776
                $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
108✔
1777
                if (! is_int($classIndex)) {
108✔
1778
                        return false;
92✔
1779
                }
1780
                $classProperties = $phpcsFile->getClassProperties($classIndex);
16✔
1781
                return $classProperties['is_abstract'];
16✔
1782
        }
1783

1784
        /**
1785
         * Return true if the function body is empty or contains only `return;`
1786
         *
1787
         * @param File $phpcsFile
1788
         * @param int  $stackPtr  The index of the function keyword.
1789
         *
1790
         * @return bool
1791
         */
1792
        public static function isFunctionBodyEmpty(File $phpcsFile, $stackPtr)
8✔
1793
        {
1794
                $tokens = $phpcsFile->getTokens();
8✔
1795
                if ($tokens[$stackPtr]['code'] !== T_FUNCTION) {
8✔
1796
                        return false;
×
1797
                }
1798
                $functionScopeStart = $tokens[$stackPtr]['scope_opener'];
8✔
1799
                $functionScopeEnd = $tokens[$stackPtr]['scope_closer'];
8✔
1800
                $tokensToIgnore = array_merge(
8✔
1801
                        Tokens::$emptyTokens,
8✔
1802
                        [
4✔
1803
                                T_RETURN,
8✔
1804
                                T_SEMICOLON,
8✔
1805
                                T_OPEN_CURLY_BRACKET,
8✔
1806
                                T_CLOSE_CURLY_BRACKET,
8✔
1807
                        ]
4✔
1808
                );
8✔
1809
                for ($i = $functionScopeStart; $i < $functionScopeEnd; $i++) {
8✔
1810
                        if (! in_array($tokens[$i]['code'], $tokensToIgnore, true)) {
8✔
1811
                                return false;
8✔
1812
                        }
1813
                }
4✔
1814
                return true;
8✔
1815
        }
1816
}
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