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

sirbrillig / phpcs-variable-analysis / 21336984339

25 Jan 2026 05:48PM UTC coverage: 94.662% (+0.9%) from 93.732%
21336984339

Pull #360

github

sirbrillig
Bump min phpcs version to 3.15.5 in GH actions
Pull Request #360: Migrate to PHPCSUtils

10 of 11 new or added lines in 1 file covered. (90.91%)

2 existing lines in 1 file now uncovered.

1862 of 1967 relevant lines covered (94.66%)

135.14 hits per line

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

91.82
/VariableAnalysis/Lib/Helpers.php
1
<?php
2

3
namespace VariableAnalysis\Lib;
4

5
use PHP_CodeSniffer\Files\File;
6
use VariableAnalysis\Lib\ScopeInfo;
7
use VariableAnalysis\Lib\Constants;
8
use VariableAnalysis\Lib\ForLoopInfo;
9
use VariableAnalysis\Lib\EnumInfo;
10
use VariableAnalysis\Lib\ScopeType;
11
use VariableAnalysis\Lib\VariableInfo;
12
use PHP_CodeSniffer\Util\Tokens;
13
use PHPCSUtils\Utils\Context;
14
use PHPCSUtils\Utils\Parentheses;
15

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

387
                return $argPtrs;
36✔
388
        }
389

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

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

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

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

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

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

456
                return $enclosingScopeIndex;
356✔
457
        }
458

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

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

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

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

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

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

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

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

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

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

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

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

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

662
                return null;
36✔
663
        }
664

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

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

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

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

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

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

736
        /**
737
         * Determine if a token is a list opener for list assignment/destructuring.
738
         *
739
         * The index provided can be either the opening square brace of a short list
740
         * assignment like the first character of `[$a] = $b;` or the `list` token of
741
         * an expression like `list($a) = $b;` or the opening parenthesis of that
742
         * expression.
743
         *
744
         * @param File $phpcsFile
745
         * @param int  $listOpenerIndex
746
         *
747
         * @return bool
748
         */
749
        private static function isListAssignment(File $phpcsFile, $listOpenerIndex)
32✔
750
        {
751
                $tokens = $phpcsFile->getTokens();
32✔
752
                // Match `[$a] = $b;` except for when the previous token is a parenthesis.
753
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SHORT_ARRAY) {
32✔
754
                        return true;
32✔
755
                }
756
                // Match `list($a) = $b;`
757
                if ($tokens[$listOpenerIndex]['code'] === T_LIST) {
32✔
758
                        return true;
32✔
759
                }
760

761
                // If $listOpenerIndex is the open parenthesis of `list($a) = $b;`, then
762
                // match that too.
763
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_PARENTHESIS) {
12✔
764
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
4✔
765
                        if (
766
                                isset($tokens[$previousTokenPtr])
4✔
767
                                && $tokens[$previousTokenPtr]['code'] === T_LIST
4✔
768
                        ) {
1✔
769
                                return true;
4✔
770
                        }
771
                        return true;
×
772
                }
773

774
                // If the list opener token is a square bracket that is preceeded by a
775
                // close parenthesis that has an owner which is a scope opener, then this
776
                // is a list assignment and not an array access.
777
                //
778
                // Match `if (true) [$a] = $b;`
779
                if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SQUARE_BRACKET) {
8✔
780
                        $previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
8✔
781
                        if (
782
                                isset($tokens[$previousTokenPtr])
8✔
783
                                && $tokens[$previousTokenPtr]['code'] === T_CLOSE_PARENTHESIS
8✔
784
                                && isset($tokens[$previousTokenPtr]['parenthesis_owner'])
8✔
785
                                && isset(Tokens::$scopeOpeners[$tokens[$tokens[$previousTokenPtr]['parenthesis_owner']]['code']])
8✔
786
                        ) {
2✔
UNCOV
787
                                return true;
×
788
                        }
789
                }
2✔
790

791
                return false;
8✔
792
        }
793

794
        /**
795
         * Return a list of indices for variables assigned within a list assignment.
796
         *
797
         * The index provided can be either the opening square brace of a short list
798
         * assignment like the first character of `[$a] = $b;` or the `list` token of
799
         * an expression like `list($a) = $b;` or the opening parenthesis of that
800
         * expression.
801
         *
802
         * @param File $phpcsFile
803
         * @param int  $listOpenerIndex
804
         *
805
         * @return ?array<int>
806
         */
807
        public static function getListAssignments(File $phpcsFile, $listOpenerIndex)
48✔
808
        {
809
                $tokens = $phpcsFile->getTokens();
48✔
810
                self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]);
48✔
811

812
                // First find the end of the list
813
                $closePtr = null;
48✔
814
                if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) {
48✔
815
                        $closePtr = $tokens[$listOpenerIndex]['parenthesis_closer'];
48✔
816
                }
12✔
817
                if (isset($tokens[$listOpenerIndex]['bracket_closer'])) {
48✔
818
                        $closePtr = $tokens[$listOpenerIndex]['bracket_closer'];
48✔
819
                }
12✔
820
                if (! $closePtr) {
48✔
821
                        return null;
×
822
                }
823

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

827
                // If the next token isn't an assignment, check for nested brackets because we might be a nested assignment
828
                if (! is_int($assignPtr) || $tokens[$assignPtr]['code'] !== T_EQUAL) {
48✔
829
                        // Collect the enclosing list open/close tokens ($parents is an assoc array keyed by opener index and the value is the closer index)
830
                        $parents = isset($tokens[$listOpenerIndex]['nested_parenthesis']) ? $tokens[$listOpenerIndex]['nested_parenthesis'] : [];
40✔
831
                        // There's no record of nested brackets for short lists; we'll have to find the parent ourselves
832
                        if (empty($parents)) {
40✔
833
                                $parentSquareBracketPtr = self::findContainingOpeningSquareBracket($phpcsFile, $listOpenerIndex);
40✔
834
                                if (is_int($parentSquareBracketPtr)) {
40✔
835
                                        // Make sure that the parent is really a parent by checking that its
836
                                        // closing index is outside of the current bracket's closing index.
837
                                        $parentSquareBracketToken = $tokens[$parentSquareBracketPtr];
4✔
838
                                        $parentSquareBracketClosePtr = $parentSquareBracketToken['bracket_closer'];
4✔
839
                                        if ($parentSquareBracketClosePtr && $parentSquareBracketClosePtr > $closePtr) {
4✔
840
                                                self::debug("found enclosing bracket for {$listOpenerIndex}: {$parentSquareBracketPtr}");
4✔
841
                                                // Collect the opening index, but we don't actually need the closing paren index so just make that 0
842
                                                $parents = [$parentSquareBracketPtr => 0];
4✔
843
                                        }
1✔
844
                                }
1✔
845
                        }
10✔
846
                        // If we have no parents, this is not a nested assignment and therefore is not an assignment
847
                        if (empty($parents)) {
40✔
848
                                return null;
40✔
849
                        }
850

851
                        // Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion)
852
                        $isNestedAssignment = null;
28✔
853
                        $parentListOpener = array_keys(array_reverse($parents, true))[0];
28✔
854
                        $isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener);
28✔
855
                        if ($isNestedAssignment === null) {
28✔
856
                                return null;
28✔
857
                        }
858
                }
1✔
859

860
                $variablePtrs = [];
32✔
861

862
                $currentPtr = $listOpenerIndex;
32✔
863
                $variablePtr = 0;
32✔
864
                while ($currentPtr < $closePtr && is_int($variablePtr)) {
32✔
865
                        $variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr);
32✔
866
                        if (is_int($variablePtr)) {
32✔
867
                                $variablePtrs[] = $variablePtr;
32✔
868
                        }
8✔
869
                        ++$currentPtr;
32✔
870
                }
8✔
871

872
                if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) {
32✔
873
                        return null;
8✔
874
                }
875

876
                return $variablePtrs;
32✔
877
        }
878

879
        /**
880
         * @param File $phpcsFile
881
         * @param int  $stackPtr
882
         *
883
         * @return string[]
884
         */
885
        public static function getVariablesDefinedByArrowFunction(File $phpcsFile, $stackPtr)
36✔
886
        {
887
                $tokens = $phpcsFile->getTokens();
36✔
888
                $arrowFunctionToken = $tokens[$stackPtr];
36✔
889
                $variableNames = [];
36✔
890
                self::debug('looking for variables in arrow function token', $arrowFunctionToken);
36✔
891
                for ($index = $arrowFunctionToken['parenthesis_opener']; $index < $arrowFunctionToken['parenthesis_closer']; $index++) {
36✔
892
                        $token = $tokens[$index];
36✔
893
                        if ($token['code'] === T_VARIABLE) {
36✔
894
                                $variableNames[] = self::normalizeVarName($token['content']);
36✔
895
                        }
9✔
896
                }
9✔
897
                self::debug('found these variables in arrow function token', $variableNames);
36✔
898
                return $variableNames;
36✔
899
        }
900

901
        /**
902
         * @return void
903
         */
904
        public static function debug()
356✔
905
        {
906
                $messages = func_get_args();
356✔
907
                if (! defined('PHP_CODESNIFFER_VERBOSITY')) {
356✔
908
                        return;
×
909
                }
910
                if (PHP_CODESNIFFER_VERBOSITY <= 3) {
356✔
911
                        return;
356✔
912
                }
913
                $output = PHP_EOL . 'VariableAnalysisSniff: DEBUG:';
×
914
                foreach ($messages as $message) {
×
915
                        if (is_string($message) || is_numeric($message)) {
×
916
                                $output .= ' "' . $message . '"';
×
917
                                continue;
×
918
                        }
919
                        $output .= PHP_EOL . var_export($message, true) . PHP_EOL;
×
920
                }
921
                $output .= PHP_EOL;
×
922
                echo $output;
×
923
        }
924

925
        /**
926
         * @param string $pattern
927
         * @param string $value
928
         *
929
         * @return string[]
930
         */
931
        public static function splitStringToArray($pattern, $value)
28✔
932
        {
933
                if (empty($pattern)) {
28✔
934
                        return [];
×
935
                }
936
                $result = preg_split($pattern, $value);
28✔
937
                return is_array($result) ? $result : [];
28✔
938
        }
939

940
        /**
941
         * @param string $varName
942
         *
943
         * @return bool
944
         */
945
        public static function isVariableANumericVariable($varName)
340✔
946
        {
947
                return is_numeric(substr($varName, 0, 1));
340✔
948
        }
949

950
        /**
951
         * @param File $phpcsFile
952
         * @param int  $stackPtr
953
         *
954
         * @return bool
955
         */
956
        public static function isVariableInsideElseCondition(File $phpcsFile, $stackPtr)
332✔
957
        {
958
                $tokens = $phpcsFile->getTokens();
332✔
959
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
332✔
960
                $nonFunctionTokenTypes[] = T_OPEN_PARENTHESIS;
332✔
961
                $nonFunctionTokenTypes[] = T_INLINE_HTML;
332✔
962
                $nonFunctionTokenTypes[] = T_CLOSE_TAG;
332✔
963
                $nonFunctionTokenTypes[] = T_VARIABLE;
332✔
964
                $nonFunctionTokenTypes[] = T_ELLIPSIS;
332✔
965
                $nonFunctionTokenTypes[] = T_COMMA;
332✔
966
                $nonFunctionTokenTypes[] = T_STRING;
332✔
967
                $nonFunctionTokenTypes[] = T_BITWISE_AND;
332✔
968
                $elsePtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $stackPtr - 1, null, true, null, true));
332✔
969
                $elseTokenTypes = [
166✔
970
                        T_ELSE,
332✔
971
                        T_ELSEIF,
332✔
972
                ];
249✔
973
                if (is_int($elsePtr) && in_array($tokens[$elsePtr]['code'], $elseTokenTypes, true)) {
332✔
974
                        return true;
16✔
975
                }
976
                return false;
332✔
977
        }
978

979
        /**
980
         * @param File $phpcsFile
981
         * @param int  $stackPtr
982
         *
983
         * @return bool
984
         */
985
        public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr)
332✔
986
        {
987
                $tokens = $phpcsFile->getTokens();
332✔
988
                $token = $tokens[$stackPtr];
332✔
989
                $conditions = isset($token['conditions']) ? $token['conditions'] : [];
332✔
990
                $elseTokenTypes = [
166✔
991
                        T_ELSE,
332✔
992
                        T_ELSEIF,
332✔
993
                ];
249✔
994
                foreach (array_reverse($conditions, true) as $scopeCode) {
332✔
995
                        if (in_array($scopeCode, $elseTokenTypes, true)) {
312✔
996
                                return true;
16✔
997
                        }
998
                }
83✔
999

1000
                // Some else body code will not have conditions because it is inline (no
1001
                // curly braces) so we have to look in other ways.
1002
                $previousSemicolonPtr = $phpcsFile->findPrevious([T_SEMICOLON], $stackPtr - 1);
332✔
1003
                if (! is_int($previousSemicolonPtr)) {
332✔
1004
                        $previousSemicolonPtr = 0;
152✔
1005
                }
38✔
1006
                $elsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1, $previousSemicolonPtr);
332✔
1007
                if (is_int($elsePtr)) {
332✔
1008
                        return true;
8✔
1009
                }
1010

1011
                return false;
332✔
1012
        }
1013

1014
        /**
1015
         * @param File $phpcsFile
1016
         * @param int  $stackPtr
1017
         *
1018
         * @return int[]
1019
         */
1020
        public static function getAttachedBlockIndicesForElse(File $phpcsFile, $stackPtr)
24✔
1021
        {
1022
                $currentElsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1);
24✔
1023
                if (! is_int($currentElsePtr)) {
24✔
1024
                        throw new \Exception("Cannot find expected else at {$stackPtr}");
×
1025
                }
1026

1027
                $ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
24✔
1028
                if (! is_int($ifPtr)) {
24✔
1029
                        throw new \Exception("Cannot find if for else at {$stackPtr}");
×
1030
                }
1031
                $blockIndices = [$ifPtr];
24✔
1032

1033
                $previousElseIfPtr = $currentElsePtr;
24✔
1034
                do {
1035
                        $elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
24✔
1036
                        if (is_int($elseIfPtr)) {
24✔
1037
                                $blockIndices[] = $elseIfPtr;
16✔
1038
                                $previousElseIfPtr = $elseIfPtr;
16✔
1039
                        }
4✔
1040
                } while (is_int($elseIfPtr));
24✔
1041

1042
                return $blockIndices;
24✔
1043
        }
1044

1045
        /**
1046
         * @param int $needle
1047
         * @param int $scopeStart
1048
         * @param int $scopeEnd
1049
         *
1050
         * @return bool
1051
         */
1052
        public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
24✔
1053
        {
1054
                return ($needle > $scopeStart && $needle < $scopeEnd);
24✔
1055
        }
1056

1057
        /**
1058
         * @param File $phpcsFile
1059
         * @param int  $scopeStartIndex
1060
         *
1061
         * @return int
1062
         */
1063
        public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
356✔
1064
        {
1065
                $tokens = $phpcsFile->getTokens();
356✔
1066
                $scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
356✔
1067

1068
                if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) {
356✔
1069
                        $arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex);
36✔
1070
                        $scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex;
36✔
1071
                }
9✔
1072

1073
                if ($scopeStartIndex === 0) {
356✔
1074
                        $scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
356✔
1075
                }
89✔
1076
                return $scopeCloserIndex;
356✔
1077
        }
1078

1079
        /**
1080
         * @param File $phpcsFile
1081
         *
1082
         * @return int
1083
         */
1084
        public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
356✔
1085
        {
1086
                $tokens = $phpcsFile->getTokens();
356✔
1087
                foreach (array_reverse($tokens, true) as $index => $token) {
356✔
1088
                        if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
356✔
1089
                                return $index;
356✔
1090
                        }
1091
                }
87✔
1092
                self::debug('no non-empty token found for end of file');
×
1093
                return 0;
×
1094
        }
1095

1096
        /**
1097
         * @param VariableInfo $varInfo
1098
         * @param ScopeInfo    $scopeInfo
1099
         *
1100
         * @return bool
1101
         */
1102
        public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo)
68✔
1103
        {
1104
                $foundVarPosition = false;
68✔
1105
                foreach ($scopeInfo->variables as $variable) {
68✔
1106
                        if ($variable === $varInfo) {
68✔
1107
                                $foundVarPosition = true;
68✔
1108
                                continue;
68✔
1109
                        }
1110
                        if (! $foundVarPosition) {
44✔
1111
                                continue;
36✔
1112
                        }
1113
                        if ($variable->scopeType !== ScopeType::PARAM) {
44✔
1114
                                continue;
36✔
1115
                        }
1116
                        if ($variable->firstRead) {
16✔
1117
                                return true;
16✔
1118
                        }
1119
                }
17✔
1120
                return false;
68✔
1121
        }
1122

1123
        /**
1124
         * @param File         $phpcsFile
1125
         * @param VariableInfo $varInfo
1126
         * @param ScopeInfo    $scopeInfo
1127
         *
1128
         * @return bool
1129
         */
1130
        public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
4✔
1131
        {
1132
                $requireTokens = [
2✔
1133
                        T_REQUIRE,
4✔
1134
                        T_REQUIRE_ONCE,
4✔
1135
                        T_INCLUDE,
4✔
1136
                        T_INCLUDE_ONCE,
4✔
1137
                ];
3✔
1138
                $indexToStartSearch = $varInfo->firstDeclared;
4✔
1139
                if (! empty($varInfo->firstInitialized)) {
4✔
1140
                        $indexToStartSearch = $varInfo->firstInitialized;
4✔
1141
                }
1✔
1142
                $tokens = $phpcsFile->getTokens();
4✔
1143
                $indexToStopSearch = isset($tokens[$scopeInfo->scopeStartIndex]['scope_closer']) ? $tokens[$scopeInfo->scopeStartIndex]['scope_closer'] : null;
4✔
1144
                if (! is_int($indexToStartSearch) || ! is_int($indexToStopSearch)) {
4✔
1145
                        return false;
×
1146
                }
1147
                $requireTokenIndex = $phpcsFile->findNext($requireTokens, $indexToStartSearch + 1, $indexToStopSearch);
4✔
1148
                if (is_int($requireTokenIndex)) {
4✔
1149
                        return true;
4✔
1150
                }
1151
                return false;
×
1152
        }
1153

1154
        /**
1155
         * Find the index of the function keyword for a token in a function call's arguments
1156
         *
1157
         * For the variable `$foo` in the expression `doSomething($foo)`, this will
1158
         * return the index of the `doSomething` token.
1159
         *
1160
         * @param File $phpcsFile
1161
         * @param int  $stackPtr
1162
         *
1163
         * @return ?int
1164
         */
1165
        public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr)
324✔
1166
        {
1167
                $tokens = $phpcsFile->getTokens();
324✔
1168
                $token = $tokens[$stackPtr];
324✔
1169
                if (empty($token['nested_parenthesis'])) {
324✔
1170
                        return null;
316✔
1171
                }
1172
                /**
1173
                 * @var list<int|string>
1174
                 */
1175
                $startingParenthesis = array_keys($token['nested_parenthesis']);
80✔
1176
                $startOfArguments = end($startingParenthesis);
80✔
1177
                if (! is_int($startOfArguments)) {
80✔
1178
                        return null;
×
1179
                }
1180

1181
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
80✔
1182
                $functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
80✔
1183
                if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
80✔
1184
                        return null;
×
1185
                }
1186
                if (
1187
                        $tokens[$functionPtr]['content'] === 'function'
80✔
1188
                        || ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))
80✔
1189
                ) {
20✔
1190
                        // If there is a function/fn keyword before the beginning of the parens,
1191
                        // this is a function definition and not a function call.
1192
                        return null;
×
1193
                }
1194
                if (! empty($tokens[$functionPtr]['scope_opener'])) {
80✔
1195
                        // If the alleged function name has a scope, this is not a function call.
1196
                        return null;
28✔
1197
                }
1198

1199
                $functionNameType = $tokens[$functionPtr]['code'];
72✔
1200
                if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
72✔
1201
                        // If the alleged function name is not a variable or a string, this is
1202
                        // not a function call.
1203
                        return null;
32✔
1204
                }
1205

1206
                if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
56✔
1207
                        // If the variable is inside a different scope than the function name,
1208
                        // the function call doesn't apply to the variable.
1209
                        return null;
32✔
1210
                }
1211

1212
                return $functionPtr;
24✔
1213
        }
1214

1215
        /**
1216
         * @param File $phpcsFile
1217
         * @param int  $stackPtr
1218
         *
1219
         * @return bool
1220
         */
1221
        public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
332✔
1222
        {
1223
                // Use PHPCSUtils which handles all edge cases across PHP/PHPCS versions
1224
                return Context::inIsset($phpcsFile, $stackPtr) || Context::inEmpty($phpcsFile, $stackPtr);
332✔
1225
        }
1226

1227
        /**
1228
         * @param File $phpcsFile
1229
         * @param int  $stackPtr
1230
         *
1231
         * @return bool
1232
         */
1233
        public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
260✔
1234
        {
1235
                $tokens = $phpcsFile->getTokens();
260✔
1236
                $nonFunctionTokenTypes = Tokens::$emptyTokens;
260✔
1237

1238
                $arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
260✔
1239
                if (! is_int($arrayPushOperatorIndex1)) {
260✔
1240
                        return false;
×
1241
                }
1242
                if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
260✔
1243
                        return false;
260✔
1244
                }
1245

1246
                $arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true));
36✔
1247
                if (! is_int($arrayPushOperatorIndex2)) {
36✔
1248
                        return false;
×
1249
                }
1250
                if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') {
36✔
1251
                        return false;
8✔
1252
                }
1253

1254
                $arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true));
28✔
1255
                if (! is_int($arrayPushOperatorIndex3)) {
28✔
1256
                        return false;
×
1257
                }
1258
                if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') {
28✔
1259
                        return false;
×
1260
                }
1261

1262
                return true;
28✔
1263
        }
1264

1265
        /**
1266
         * @param File $phpcsFile
1267
         * @param int  $stackPtr
1268
         *
1269
         * @return bool
1270
         */
1271
        public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
260✔
1272
        {
1273
                // Use PHPCSUtils which handles all edge cases across PHP/PHPCS versions
1274
                return Context::inUnset($phpcsFile, $stackPtr);
260✔
1275
        }
1276

1277
        /**
1278
         * @param File $phpcsFile
1279
         * @param int  $stackPtr
1280
         *
1281
         * @return bool
1282
         */
1283
        public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr)
324✔
1284
        {
1285
                $previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
324✔
1286
                if (! is_int($previousStatementPtr)) {
324✔
1287
                        $previousStatementPtr = 1;
40✔
1288
                }
10✔
1289
                $previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
324✔
1290
                if (is_int($previousTokenPtr)) {
324✔
1291
                        return true;
4✔
1292
                }
1293
                return false;
324✔
1294
        }
1295

1296
        /**
1297
         * @param File $phpcsFile
1298
         * @param int  $stackPtr
1299
         *
1300
         * @return bool
1301
         */
1302
        public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
344✔
1303
        {
1304
                // Is the next non-whitespace an assignment?
1305
                $assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
344✔
1306
                if (! is_int($assignPtr)) {
344✔
1307
                        return false;
336✔
1308
                }
1309

1310
                // Is this a variable variable? If so, it's not an assignment to the current variable.
1311
                if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
324✔
1312
                        self::debug('found variable variable');
4✔
1313
                        return false;
4✔
1314
                }
1315
                return true;
324✔
1316
        }
1317

1318
        /**
1319
         * @param File $phpcsFile
1320
         * @param int  $stackPtr
1321
         *
1322
         * @return bool
1323
         */
1324
        public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
324✔
1325
        {
1326
                $tokens = $phpcsFile->getTokens();
324✔
1327

1328
                $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
324✔
1329
                if ($prev === false) {
324✔
1330
                        return false;
×
1331
                }
1332
                if ($tokens[$prev]['code'] === T_DOLLAR) {
324✔
1333
                        return true;
4✔
1334
                }
1335
                if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
324✔
1336
                        return false;
268✔
1337
                }
1338

1339
                $prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
232✔
1340
                if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
232✔
1341
                        return true;
×
1342
                }
1343
                return false;
232✔
1344
        }
1345

1346
        /**
1347
         * @param File $phpcsFile
1348
         * @param int  $stackPtr
1349
         *
1350
         * @return EnumInfo|null
1351
         */
1352
        public static function makeEnumInfo(File $phpcsFile, $stackPtr)
4✔
1353
        {
1354
                $tokens = $phpcsFile->getTokens();
4✔
1355
                $token = $tokens[$stackPtr];
4✔
1356

1357
                if (isset($token['scope_opener'])) {
4✔
1358
                        $blockStart = $token['scope_opener'];
4✔
1359
                        $blockEnd = $token['scope_closer'];
4✔
1360
                } else {
1✔
1361
                        // Enums before phpcs could detect them do not have scopes so we have to
1362
                        // find them ourselves.
1363

1364
                        $blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
4✔
1365
                        if (! is_int($blockStart)) {
4✔
1366
                                return null;
4✔
1367
                        }
UNCOV
1368
                        $blockEnd = $tokens[$blockStart]['bracket_closer'];
×
1369
                }
1370

1371
                return new EnumInfo(
4✔
1372
                        $stackPtr,
4✔
1373
                        $blockStart,
4✔
1374
                        $blockEnd
3✔
1375
                );
3✔
1376
        }
1377

1378
        /**
1379
         * @param File $phpcsFile
1380
         * @param int  $stackPtr
1381
         *
1382
         * @return ForLoopInfo
1383
         */
1384
        public static function makeForLoopInfo(File $phpcsFile, $stackPtr)
8✔
1385
        {
1386
                $tokens = $phpcsFile->getTokens();
8✔
1387
                $token = $tokens[$stackPtr];
8✔
1388
                $forIndex = $stackPtr;
8✔
1389
                $blockStart = $token['parenthesis_closer'];
8✔
1390
                if (isset($token['scope_opener'])) {
8✔
1391
                        $blockStart = $token['scope_opener'];
8✔
1392
                        $blockEnd = $token['scope_closer'];
8✔
1393
                } else {
2✔
1394
                        // Some for loop blocks will not have scope positions because it they are
1395
                        // inline (no curly braces) so we have to find the end of their scope by
1396
                        // looking for the end of the next statement.
1397
                        $nextSemicolonIndex = $phpcsFile->findNext([T_SEMICOLON], $token['parenthesis_closer']);
8✔
1398
                        if (! is_int($nextSemicolonIndex)) {
8✔
1399
                                $nextSemicolonIndex = $token['parenthesis_closer'] + 1;
×
1400
                        }
1401
                        $blockEnd = $nextSemicolonIndex;
8✔
1402
                }
1403
                $initStart = intval($token['parenthesis_opener']) + 1;
8✔
1404
                $initEnd = null;
8✔
1405
                $conditionStart = null;
8✔
1406
                $conditionEnd = null;
8✔
1407
                $incrementStart = null;
8✔
1408
                $incrementEnd = $token['parenthesis_closer'] - 1;
8✔
1409

1410
                $semicolonCount = 0;
8✔
1411
                $forLoopLevel = $tokens[$forIndex]['level'];
8✔
1412
                $forLoopNestedParensCount = 1;
8✔
1413

1414
                if (isset($tokens[$forIndex]['nested_parenthesis'])) {
8✔
1415
                        $forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
×
1416
                }
1417

1418
                for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
8✔
1419
                        if ($tokens[$i]['code'] !== T_SEMICOLON) {
8✔
1420
                                continue;
8✔
1421
                        }
1422

1423
                        if ($tokens[$i]['level'] !== $forLoopLevel) {
8✔
1424
                                continue;
8✔
1425
                        }
1426

1427
                        if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
8✔
1428
                                continue;
×
1429
                        }
1430

1431
                        switch ($semicolonCount) {
1432
                                case 0:
8✔
1433
                                        $initEnd = $i;
8✔
1434
                                        $conditionStart = $initEnd + 1;
8✔
1435
                                        break;
8✔
1436
                                case 1:
8✔
1437
                                        $conditionEnd = $i;
8✔
1438
                                        $incrementStart = $conditionEnd + 1;
8✔
1439
                                        break;
8✔
1440
                        }
1441
                        $semicolonCount += 1;
8✔
1442
                }
2✔
1443

1444
                if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
8✔
1445
                        throw new \Exception("Cannot parse for loop at position {$forIndex}");
×
1446
                }
1447

1448
                return new ForLoopInfo(
8✔
1449
                        $forIndex,
8✔
1450
                        $blockStart,
8✔
1451
                        $blockEnd,
8✔
1452
                        $initStart,
8✔
1453
                        $initEnd,
8✔
1454
                        $conditionStart,
8✔
1455
                        $conditionEnd,
8✔
1456
                        $incrementStart,
8✔
1457
                        $incrementEnd
6✔
1458
                );
6✔
1459
        }
1460

1461
        /**
1462
         * @param int                     $stackPtr
1463
         * @param array<int, ForLoopInfo> $forLoops
1464
         * @return ForLoopInfo|null
1465
         */
1466
        public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
352✔
1467
        {
1468
                foreach ($forLoops as $forLoop) {
352✔
1469
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
1470
                                return $forLoop;
8✔
1471
                        }
1472
                }
88✔
1473
                return null;
352✔
1474
        }
1475

1476
        /**
1477
         * Return true if the token looks like constructor promotion.
1478
         *
1479
         * Call on a parameter variable token only.
1480
         *
1481
         * @param File $phpcsFile
1482
         * @param int  $stackPtr
1483
         *
1484
         * @return bool
1485
         */
1486
        public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
232✔
1487
        {
1488
                // If we are not in a function's parameters, this is not promotion.
1489
                $functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
232✔
1490
                if (! $functionIndex) {
232✔
1491
                        return false;
×
1492
                }
1493

1494
                $tokens = $phpcsFile->getTokens();
232✔
1495

1496
                // Move backwards from the token, ignoring whitespace, typehints, and the
1497
                // 'readonly' keyword, and return true if the previous token is a
1498
                // visibility keyword (eg: `public`).
1499
                for ($i = $stackPtr - 1; $i > $functionIndex; $i--) {
232✔
1500
                        if (in_array($tokens[$i]['code'], Tokens::$scopeModifiers, true)) {
232✔
1501
                                return true;
12✔
1502
                        }
1503
                        if (in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) {
232✔
1504
                                continue;
160✔
1505
                        }
1506
                        if ($tokens[$i]['content'] === 'readonly') {
232✔
1507
                                continue;
8✔
1508
                        }
1509
                        if (self::isTokenPartOfTypehint($phpcsFile, $i)) {
232✔
1510
                                continue;
32✔
1511
                        }
1512
                        return false;
228✔
1513
                }
1514
                return false;
×
1515
        }
1516

1517
        /**
1518
         * If looking at a function call token, return a string for the full function
1519
         * name including any inline namespace.
1520
         *
1521
         * So for example, if the call looks like `\My\Namespace\doSomething($bar)`
1522
         * and `$stackPtr` refers to `doSomething`, this will return
1523
         * `\My\Namespace\doSomething`.
1524
         *
1525
         * @param File $phpcsFile
1526
         * @param int  $stackPtr
1527
         *
1528
         * @return string|null
1529
         */
1530
        public static function getFunctionNameWithNamespace(File $phpcsFile, $stackPtr)
156✔
1531
        {
1532
                $tokens = $phpcsFile->getTokens();
156✔
1533

1534
                if (! isset($tokens[$stackPtr])) {
156✔
1535
                        return null;
×
1536
                }
1537
                $startOfScope = self::findVariableScope($phpcsFile, $stackPtr);
156✔
1538
                $functionName = $tokens[$stackPtr]['content'];
156✔
1539

1540
                // In PHPCS 4.x, T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, and T_NAME_RELATIVE
1541
                // tokens already contain the full namespaced name, so we can return early.
1542
                if ($tokens[$stackPtr]['code'] === T_NAME_FULLY_QUALIFIED) {
156✔
1543
                        return $functionName;
8✔
1544
                }
1545
                if ($tokens[$stackPtr]['code'] === T_NAME_QUALIFIED) {
156✔
1546
                        return $functionName;
×
1547
                }
1548
                if ($tokens[$stackPtr]['code'] === T_NAME_RELATIVE) {
156✔
1549
                        return $functionName;
×
1550
                }
1551

1552
                // Move backwards from the token, collecting namespace separators and
1553
                // strings, until we encounter whitespace or something else.
1554
                $partOfNamespace = [
78✔
1555
                        T_NS_SEPARATOR,
156✔
1556
                        T_STRING,
156✔
1557
                        T_NAME_QUALIFIED,
156✔
1558
                        T_NAME_RELATIVE,
156✔
1559
                        T_NAME_FULLY_QUALIFIED,
156✔
1560
                ];
117✔
1561
                for ($i = $stackPtr - 1; $i > $startOfScope; $i--) {
156✔
1562
                        if (! in_array($tokens[$i]['code'], $partOfNamespace, true)) {
156✔
1563
                                break;
156✔
1564
                        }
1565
                        $functionName = "{$tokens[$i]['content']}{$functionName}";
8✔
1566
                }
4✔
1567
                return $functionName;
156✔
1568
        }
1569

1570
        /**
1571
         * Return false if the token is definitely not part of a typehint
1572
         *
1573
         * @param File $phpcsFile
1574
         * @param int  $stackPtr
1575
         *
1576
         * @return bool
1577
         */
1578
        private static function isTokenPossiblyPartOfTypehint(File $phpcsFile, $stackPtr)
232✔
1579
        {
1580
                $tokens = $phpcsFile->getTokens();
232✔
1581
                $token = $tokens[$stackPtr];
232✔
1582
                if ($token['code'] === 'PHPCS_T_NULLABLE') {
232✔
1583
                        return true;
8✔
1584
                }
1585
                if ($token['code'] === T_NAME_QUALIFIED) {
232✔
1586
                        return true;
×
1587
                }
1588
                if ($token['code'] === T_NAME_RELATIVE) {
232✔
1589
                        return true;
×
1590
                }
1591
                if ($token['code'] === T_NAME_FULLY_QUALIFIED) {
232✔
1592
                        return true;
12✔
1593
                }
1594
                if ($token['code'] === T_NS_SEPARATOR) {
232✔
1595
                        return true;
12✔
1596
                }
1597
                if ($token['code'] === T_STRING) {
232✔
1598
                        return true;
32✔
1599
                }
1600
                if ($token['code'] === T_TRUE) {
232✔
1601
                        return true;
8✔
1602
                }
1603
                if ($token['code'] === T_FALSE) {
232✔
1604
                        return true;
8✔
1605
                }
1606
                if ($token['code'] === T_NULL) {
232✔
1607
                        return true;
8✔
1608
                }
1609
                if ($token['content'] === '|') {
232✔
1610
                        return true;
16✔
1611
                }
1612
                if (in_array($token['code'], Tokens::$emptyTokens)) {
232✔
1613
                        return true;
32✔
1614
                }
1615
                return false;
232✔
1616
        }
1617

1618
        /**
1619
         * Return true if the token is inside a typehint
1620
         *
1621
         * @param File $phpcsFile
1622
         * @param int  $stackPtr
1623
         *
1624
         * @return bool
1625
         */
1626
        public static function isTokenPartOfTypehint(File $phpcsFile, $stackPtr)
232✔
1627
        {
1628
                $tokens = $phpcsFile->getTokens();
232✔
1629

1630
                if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $stackPtr)) {
232✔
1631
                        return false;
228✔
1632
                }
1633

1634
                // Examine every following token, ignoring everything that might be part of
1635
                // a typehint. If we find a variable at the end, this is part of a
1636
                // typehint.
1637
                $i = $stackPtr;
32✔
1638
                while (true) {
32✔
1639
                        $i += 1;
32✔
1640
                        if (! isset($tokens[$i])) {
32✔
1641
                                return false;
×
1642
                        }
1643
                        if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $i)) {
32✔
1644
                                return ($tokens[$i]['code'] === T_VARIABLE);
32✔
1645
                        }
1646
                }
8✔
1647
        }
1648

1649
        /**
1650
         * Return true if the token is inside an abstract class.
1651
         *
1652
         * @param File $phpcsFile
1653
         * @param int  $stackPtr
1654
         *
1655
         * @return bool
1656
         */
1657
        public static function isInAbstractClass(File $phpcsFile, $stackPtr)
108✔
1658
        {
1659
                $classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
108✔
1660
                if (! is_int($classIndex)) {
108✔
1661
                        return false;
92✔
1662
                }
1663
                $classProperties = $phpcsFile->getClassProperties($classIndex);
16✔
1664
                return $classProperties['is_abstract'];
16✔
1665
        }
1666

1667
        /**
1668
         * Return true if the function body is empty or contains only `return;`
1669
         *
1670
         * @param File $phpcsFile
1671
         * @param int  $stackPtr  The index of the function keyword.
1672
         *
1673
         * @return bool
1674
         */
1675
        public static function isFunctionBodyEmpty(File $phpcsFile, $stackPtr)
8✔
1676
        {
1677
                $tokens = $phpcsFile->getTokens();
8✔
1678
                if ($tokens[$stackPtr]['code'] !== T_FUNCTION) {
8✔
1679
                        return false;
×
1680
                }
1681
                $functionScopeStart = $tokens[$stackPtr]['scope_opener'];
8✔
1682
                $functionScopeEnd = $tokens[$stackPtr]['scope_closer'];
8✔
1683
                $tokensToIgnore = array_merge(
8✔
1684
                        Tokens::$emptyTokens,
8✔
1685
                        [
4✔
1686
                                T_RETURN,
8✔
1687
                                T_SEMICOLON,
8✔
1688
                                T_OPEN_CURLY_BRACKET,
8✔
1689
                                T_CLOSE_CURLY_BRACKET,
8✔
1690
                        ]
4✔
1691
                );
6✔
1692
                for ($i = $functionScopeStart; $i < $functionScopeEnd; $i++) {
8✔
1693
                        if (! in_array($tokens[$i]['code'], $tokensToIgnore, true)) {
8✔
1694
                                return false;
8✔
1695
                        }
1696
                }
2✔
1697
                return true;
8✔
1698
        }
1699
}
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