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

sirbrillig / phpcs-variable-analysis / 21342880106

26 Jan 2026 01:03AM UTC coverage: 94.314% (+0.6%) from 93.732%
21342880106

Pull #360

github

sirbrillig
Update README for min phpcs version
Pull Request #360: Migrate to PHPCSUtils

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

6 existing lines in 2 files now uncovered.

1808 of 1917 relevant lines covered (94.31%)

137.86 hits per line

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

95.13
/VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php
1
<?php
2

3
namespace VariableAnalysis\Sniffs\CodeAnalysis;
4

5
use VariableAnalysis\Lib\ScopeInfo;
6
use VariableAnalysis\Lib\ScopeType;
7
use VariableAnalysis\Lib\VariableInfo;
8
use VariableAnalysis\Lib\Constants;
9
use VariableAnalysis\Lib\Helpers;
10
use VariableAnalysis\Lib\ScopeManager;
11
use PHP_CodeSniffer\Sniffs\Sniff;
12
use PHP_CodeSniffer\Files\File;
13
use PHP_CodeSniffer\Util\Tokens;
14

15
class VariableAnalysisSniff implements Sniff
16
{
17
        /**
18
         * The current phpcsFile being checked.
19
         *
20
         * @var File|null
21
         */
22
        protected $currentFile = null;
23

24
        /**
25
         * @var ScopeManager
26
         */
27
        private $scopeManager;
28

29
        /**
30
         * A list of for loops, keyed by the index of their first token in this file.
31
         *
32
         * @var array<int, \VariableAnalysis\Lib\ForLoopInfo>
33
         */
34
        private $forLoops = [];
35

36
        /**
37
         * A list of enum blocks, keyed by the index of their first token in this file.
38
         *
39
         * @var array<int, \VariableAnalysis\Lib\EnumInfo>
40
         */
41
        private $enums = [];
42

43
        /**
44
         * A list of custom functions which pass in variables to be initialized by
45
         * reference (eg `preg_match()`) and therefore should not require those
46
         * variables to be defined ahead of time. The list is space separated and
47
         * each entry is of the form `functionName:1,2`. The function name comes
48
         * first followed by a colon and a comma-separated list of argument numbers
49
         * (starting from 1) which should be considered variable definitions. The
50
         * special value `...` in the arguments list will cause all arguments after
51
         * the last number to be considered variable definitions.
52
         *
53
         * @var string|null
54
         */
55
        public $sitePassByRefFunctions = null;
56

57
        /**
58
         * If set, allows common WordPress pass-by-reference functions in addition to
59
         * the standard PHP ones.
60
         *
61
         * @var bool
62
         */
63
        public $allowWordPressPassByRefFunctions = false;
64

65
        /**
66
         *  Allow exceptions in a catch block to be unused without warning.
67
         *
68
         *  @var bool
69
         */
70
        public $allowUnusedCaughtExceptions = true;
71

72
        /**
73
         *  Allow function parameters to be unused without provoking unused-var warning.
74
         *
75
         *  @var bool
76
         */
77
        public $allowUnusedFunctionParameters = false;
78

79
        /**
80
         *  If set, ignores undefined variables in the file scope (the top-level
81
         *  scope of a file).
82
         *
83
         *  @var bool
84
         */
85
        public $allowUndefinedVariablesInFileScope = false;
86

87
        /**
88
         *  If set, ignores unused variables in the file scope (the top-level
89
         *  scope of a file).
90
         *
91
         *  @var bool
92
         */
93
        public $allowUnusedVariablesInFileScope = false;
94

95
        /**
96
         *  A space-separated list of names of placeholder variables that you want to
97
         *  ignore from unused variable warnings. For example, to ignore the variables
98
         *  `$junk` and `$unused`, this could be set to `'junk unused'`.
99
         *
100
         *  @var string|null
101
         */
102
        public $validUnusedVariableNames = null;
103

104
        /**
105
         *  A PHP regexp string for variables that you want to ignore from unused
106
         *  variable warnings. For example, to ignore the variables `$_junk` and
107
         *  `$_unused`, this could be set to `'/^_/'`.
108
         *
109
         *  @var string|null
110
         */
111
        public $ignoreUnusedRegexp = null;
112

113
        /**
114
         *  A space-separated list of names of placeholder variables that you want to
115
         *  ignore from undefined variable warnings. For example, to ignore the variables
116
         *  `$post` and `$undefined`, this could be set to `'post undefined'`.
117
         *
118
         *  @var string|null
119
         */
120
        public $validUndefinedVariableNames = null;
121

122
        /**
123
         *  A PHP regexp string for variables that you want to ignore from undefined
124
         *  variable warnings. For example, to ignore the variables `$_junk` and
125
         *  `$_unused`, this could be set to `'/^_/'`.
126
         *
127
         *  @var string|null
128
         */
129
        public $validUndefinedVariableRegexp = null;
130

131
        /**
132
         * Allows unused arguments in a function definition if they are
133
         * followed by an argument which is used.
134
         *
135
         *  @var bool
136
         */
137
        public $allowUnusedParametersBeforeUsed = true;
138

139
        /**
140
         * If set to true, unused values from the `key => value` syntax
141
         * in a `foreach` loop will never be marked as unused.
142
         *
143
         *  @var bool
144
         */
145
        public $allowUnusedForeachVariables = true;
146

147
        /**
148
         * If set to true, unused variables in a function before a require or import
149
         * statement will not be marked as unused because they may be used in the
150
         * required file.
151
         *
152
         *  @var bool
153
         */
154
        public $allowUnusedVariablesBeforeRequire = false;
155

156
        /**
157
         * A cache for getPassByReferenceFunctions
158
         *
159
         * @var array<array<int|string>>|null
160
         */
161
        private $passByRefFunctionsCache = null;
162

163
        public function __construct()
356✔
164
        {
165
                $this->scopeManager = new ScopeManager();
356✔
166
        }
178✔
167

168
        /**
169
         * Decide which tokens to scan.
170
         *
171
         * @return (int|string)[]
172
         */
173
        #[\Override]
89✔
174
        public function register()
267✔
175
        {
176
                $types = [
178✔
177
                        T_VARIABLE,
356✔
178
                        T_DOUBLE_QUOTED_STRING,
356✔
179
                        T_HEREDOC,
356✔
180
                        T_CLOSE_CURLY_BRACKET,
356✔
181
                        T_FUNCTION,
356✔
182
                        T_CLOSURE,
356✔
183
                        T_STRING,
356✔
184
                        T_COMMA,
356✔
185
                        T_SEMICOLON,
356✔
186
                        T_CLOSE_PARENTHESIS,
356✔
187
                        T_FOR,
356✔
188
                        T_ENDFOR,
356✔
189
                ];
267✔
190
                if (defined('T_FN')) {
356✔
191
                        $types[] = T_FN;
356✔
192
                }
89✔
193
                if (defined('T_ENUM')) {
356✔
194
                        $types[] = T_ENUM;
356✔
195
                }
89✔
196
                return $types;
356✔
197
        }
198

199
        /**
200
         * @param string $functionName
201
         *
202
         * @return array<int|string>
203
         */
204
        private function getPassByReferenceFunction($functionName)
160✔
205
        {
206
                $passByRefFunctions = $this->getPassByReferenceFunctions();
160✔
207
                return isset($passByRefFunctions[$functionName]) ? $passByRefFunctions[$functionName] : [];
160✔
208
        }
209

210
        /**
211
         * @return array<array<int|string>>
212
         */
213
        private function getPassByReferenceFunctions()
160✔
214
        {
215
                if (! is_null($this->passByRefFunctionsCache)) {
160✔
216
                        return $this->passByRefFunctionsCache;
160✔
217
                }
218
                $passByRefFunctions = Constants::getPassByReferenceFunctions();
160✔
219
                if (!empty($this->sitePassByRefFunctions)) {
160✔
220
                        $lines = Helpers::splitStringToArray('/\s+/', trim($this->sitePassByRefFunctions));
8✔
221
                        foreach ($lines as $line) {
8✔
222
                                list ($function, $args) = explode(':', $line);
8✔
223
                                $passByRefFunctions[$function] = explode(',', $args);
8✔
224
                        }
2✔
225
                }
2✔
226
                if ($this->allowWordPressPassByRefFunctions) {
160✔
227
                        $passByRefFunctions = array_merge($passByRefFunctions, Constants::getWordPressPassByReferenceFunctions());
4✔
228
                }
1✔
229
                $this->passByRefFunctionsCache = $passByRefFunctions;
160✔
230
                return $passByRefFunctions;
160✔
231
        }
232

233
        /**
234
         * Scan and process a token.
235
         *
236
         * This is the main processing function of the sniff. Will run on every token
237
         * for which `register()` returns true.
238
         *
239
         * @param File $phpcsFile
240
         * @param int  $stackPtr
241
         *
242
         * @return void
243
         */
244
        #[\Override]
89✔
245
        public function process(File $phpcsFile, $stackPtr)
267✔
246
        {
247
                $tokens = $phpcsFile->getTokens();
356✔
248

249
                $scopeStartTokenTypes = [
178✔
250
                        T_FUNCTION,
356✔
251
                        T_CLOSURE,
356✔
252
                ];
267✔
253

254
                $token = $tokens[$stackPtr];
356✔
255

256
                // Cache the current PHPCS File in an instance variable so it can be more
257
                // easily accessed in other places which aren't passed the object.
258
                if ($this->currentFile !== $phpcsFile) {
356✔
259
                        $this->currentFile = $phpcsFile;
356✔
260
                        $this->forLoops = [];
356✔
261
                        $this->enums = [];
356✔
262
                }
89✔
263

264
                // Add the global scope for the current file to our scope indexes.
265
                $scopesForFilename = $this->scopeManager->getScopesForFilename($phpcsFile->getFilename());
356✔
266
                if (empty($scopesForFilename)) {
356✔
267
                        $this->scopeManager->recordScopeStartAndEnd($phpcsFile, 0);
356✔
268
                }
89✔
269

270
                // Find and process variables to perform two jobs: to record variable
271
                // definition or use, and to report variables as undefined if they are used
272
                // without having been first defined.
273
                if ($token['code'] === T_VARIABLE) {
356✔
274
                        $this->processVariable($phpcsFile, $stackPtr);
356✔
275
                }
89✔
276

277
                // Report variables defined but not used in the current scope as unused
278
                // variables if the current token closes scopes.
279
                $this->searchForAndProcessClosingScopesAt($phpcsFile, $stackPtr);
356✔
280

281
                // Scan variables that were postponed because they exist in the increment
282
                // expression of a for loop if the current token closes a loop.
283
                $this->processClosingForLoopsAt($phpcsFile, $stackPtr);
356✔
284

285
                if ($token['code'] === T_VARIABLE) {
356✔
286
                        return;
356✔
287
                }
288

289
                if (($token['code'] === T_DOUBLE_QUOTED_STRING) || ($token['code'] === T_HEREDOC)) {
356✔
290
                        $this->processVariableInString($phpcsFile, $stackPtr);
168✔
291
                        return;
168✔
292
                }
293
                if (($token['code'] === T_STRING) && ($token['content'] === 'compact')) {
356✔
294
                        $this->processCompact($phpcsFile, $stackPtr);
12✔
295
                        return;
12✔
296
                }
297

298
                // Record for loop boundaries so we can delay scanning the third for loop
299
                // expression until after the loop has been scanned.
300
                if ($token['code'] === T_FOR) {
356✔
301
                        $this->recordForLoop($phpcsFile, $stackPtr);
8✔
302
                        return;
8✔
303
                }
304

305
                // Record enums so we can detect them even before phpcs was able to.
306
                if ($token['content'] === 'enum') {
356✔
307
                        $enumInfo = Helpers::makeEnumInfo($phpcsFile, $stackPtr);
4✔
308
                        // The token might not actually be an enum so let's avoid returning if
309
                        // it's not.
310
                        if ($enumInfo) {
4✔
311
                                $this->enums[$stackPtr] = $enumInfo;
4✔
312
                                return;
4✔
313
                        }
314
                }
1✔
315

316
                // If the current token is a call to `get_defined_vars()`, consider that a
317
                // usage of all variables in the current scope.
318
                if ($this->isGetDefinedVars($phpcsFile, $stackPtr)) {
356✔
319
                        Helpers::debug('get_defined_vars is being called');
4✔
320
                        $this->markAllVariablesRead($phpcsFile, $stackPtr);
4✔
321
                        return;
4✔
322
                }
323

324
                // If the current token starts a scope, record that scope's start and end
325
                // indexes so that we can determine if variables in that scope are defined
326
                // and/or used.
327
                if (
328
                        in_array($token['code'], $scopeStartTokenTypes, true) ||
356✔
329
                        Helpers::isArrowFunction($phpcsFile, $stackPtr)
356✔
330
                ) {
89✔
331
                        Helpers::debug('found scope condition', $token);
340✔
332
                        $this->scopeManager->recordScopeStartAndEnd($phpcsFile, $stackPtr);
340✔
333
                        return;
340✔
334
                }
335
        }
178✔
336

337
        /**
338
         * Record the boundaries of a for loop.
339
         *
340
         * @param File $phpcsFile
341
         * @param int  $stackPtr
342
         *
343
         * @return void
344
         */
345
        private function recordForLoop($phpcsFile, $stackPtr)
8✔
346
        {
347
                $this->forLoops[$stackPtr] = Helpers::makeForLoopInfo($phpcsFile, $stackPtr);
8✔
348
        }
4✔
349

350
        /**
351
         * Find scopes closed by a token and process their variables.
352
         *
353
         * Calls `processScopeClose()` for each closed scope.
354
         *
355
         * @param File $phpcsFile
356
         * @param int  $stackPtr
357
         *
358
         * @return void
359
         */
360
        private function searchForAndProcessClosingScopesAt($phpcsFile, $stackPtr)
356✔
361
        {
362
                $scopeIndicesThisCloses = $this->scopeManager->getScopesForScopeEnd($phpcsFile->getFilename(), $stackPtr);
356✔
363

364
                $tokens = $phpcsFile->getTokens();
356✔
365
                $token = $tokens[$stackPtr];
356✔
366
                $line = $token['line'];
356✔
367
                foreach ($scopeIndicesThisCloses as $scopeIndexThisCloses) {
356✔
368
                        Helpers::debug('found closing scope at index', $stackPtr, 'line', $line, 'for scopes starting at:', $scopeIndexThisCloses->scopeStartIndex);
356✔
369
                        $this->processScopeClose($phpcsFile, $scopeIndexThisCloses->scopeStartIndex);
356✔
370
                }
89✔
371
        }
178✔
372

373
        /**
374
         * Scan variables that were postponed because they exist in the increment expression of a for loop.
375
         *
376
         * @param File $phpcsFile
377
         * @param int  $stackPtr
378
         *
379
         * @return void
380
         */
381
        private function processClosingForLoopsAt($phpcsFile, $stackPtr)
356✔
382
        {
383
                $forLoopsThisCloses = [];
356✔
384
                foreach ($this->forLoops as $forLoop) {
356✔
385
                        if ($forLoop->blockEnd === $stackPtr) {
8✔
386
                                $forLoopsThisCloses[] = $forLoop;
8✔
387
                        }
2✔
388
                }
89✔
389

390
                foreach ($forLoopsThisCloses as $forLoop) {
356✔
391
                        foreach ($forLoop->incrementVariables as $varIndex => $varInfo) {
8✔
392
                                Helpers::debug('processing delayed for loop increment variable at', $varIndex, $varInfo);
8✔
393
                                $this->processVariable($phpcsFile, $varIndex, ['ignore-for-loops' => true]);
8✔
394
                        }
2✔
395
                }
89✔
396
        }
178✔
397

398
        /**
399
         * Return true if the token is a call to `get_defined_vars()`.
400
         *
401
         * @param File $phpcsFile
402
         * @param int  $stackPtr
403
         *
404
         * @return bool
405
         */
406
        protected function isGetDefinedVars(File $phpcsFile, $stackPtr)
356✔
407
        {
408
                $tokens = $phpcsFile->getTokens();
356✔
409
                $token = $tokens[$stackPtr];
356✔
410
                if (! $token || $token['content'] !== 'get_defined_vars') {
356✔
411
                        return false;
356✔
412
                }
413
                // Make sure this is a function call
414
                $parenPointer = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
4✔
415
                if (! $parenPointer || $tokens[$parenPointer]['code'] !== T_OPEN_PARENTHESIS) {
4✔
416
                        return false;
×
417
                }
418
                return true;
4✔
419
        }
420

421
        /**
422
         * @return string
423
         */
424
        protected function getFilename()
344✔
425
        {
426
                return $this->currentFile ? $this->currentFile->getFilename() : 'unknown file';
344✔
427
        }
428

429
        /**
430
         * @param int $currScope
431
         *
432
         * @return ScopeInfo
433
         */
434
        protected function getOrCreateScopeInfo($currScope)
344✔
435
        {
436
                $scope = $this->scopeManager->getScopeForScopeStart($this->getFilename(), $currScope);
344✔
437
                if (! $scope) {
344✔
438
                        if (! $this->currentFile) {
×
439
                                throw new \Exception('Cannot create scope info; current file is not set.');
×
440
                        }
441
                        $scope = $this->scopeManager->recordScopeStartAndEnd($this->currentFile, $currScope);
×
442
                }
443
                return $scope;
344✔
444
        }
445

446
        /**
447
         * @param string $varName
448
         * @param int    $currScope
449
         *
450
         * @return VariableInfo|null
451
         */
452
        protected function getVariableInfo($varName, $currScope)
24✔
453
        {
454
                $scopeInfo = $this->scopeManager->getScopeForScopeStart($this->getFilename(), $currScope);
24✔
455
                return ($scopeInfo && isset($scopeInfo->variables[$varName])) ? $scopeInfo->variables[$varName] : null;
24✔
456
        }
457

458
        /**
459
         * Returns variable data for a variable at an index.
460
         *
461
         * The variable will also be added to the list of variables stored in its
462
         * scope so that its use or non-use can be reported when those scopes end by
463
         * `processScopeClose()`.
464
         *
465
         * @param string $varName
466
         * @param int    $currScope
467
         *
468
         * @return VariableInfo
469
         */
470
        protected function getOrCreateVariableInfo($varName, $currScope)
344✔
471
        {
472
                Helpers::debug("getOrCreateVariableInfo: starting for '{$varName}'");
344✔
473
                $scopeInfo = $this->getOrCreateScopeInfo($currScope);
344✔
474
                if (isset($scopeInfo->variables[$varName])) {
344✔
475
                        Helpers::debug("getOrCreateVariableInfo: found variable for '{$varName}'", $scopeInfo->variables[$varName]);
344✔
476
                        return $scopeInfo->variables[$varName];
344✔
477
                }
478
                Helpers::debug("getOrCreateVariableInfo: creating a new variable for '{$varName}' in scope", $scopeInfo);
344✔
479
                $scopeInfo->variables[$varName] = new VariableInfo($varName);
344✔
480
                $validUnusedVariableNames = (empty($this->validUnusedVariableNames))
344✔
481
                ? []
341✔
482
                : Helpers::splitStringToArray('/\s+/', trim($this->validUnusedVariableNames));
259✔
483
                $validUndefinedVariableNames = (empty($this->validUndefinedVariableNames))
344✔
484
                ? []
332✔
485
                : Helpers::splitStringToArray('/\s+/', trim($this->validUndefinedVariableNames));
262✔
486
                if (in_array($varName, $validUnusedVariableNames)) {
344✔
487
                        $scopeInfo->variables[$varName]->ignoreUnused = true;
4✔
488
                }
1✔
489
                if (! empty($this->ignoreUnusedRegexp) && preg_match($this->ignoreUnusedRegexp, $varName) === 1) {
344✔
490
                        $scopeInfo->variables[$varName]->ignoreUnused = true;
8✔
491
                }
2✔
492
                if ($scopeInfo->scopeStartIndex === 0 && $this->allowUndefinedVariablesInFileScope) {
344✔
493
                        $scopeInfo->variables[$varName]->ignoreUndefined = true;
4✔
494
                }
1✔
495
                if (in_array($varName, $validUndefinedVariableNames)) {
344✔
496
                        $scopeInfo->variables[$varName]->ignoreUndefined = true;
16✔
497
                }
4✔
498
                if (! empty($this->validUndefinedVariableRegexp) && preg_match($this->validUndefinedVariableRegexp, $varName) === 1) {
344✔
499
                        $scopeInfo->variables[$varName]->ignoreUndefined = true;
4✔
500
                }
1✔
501
                Helpers::debug("getOrCreateVariableInfo: scope for '{$varName}' is now", $scopeInfo);
344✔
502
                return $scopeInfo->variables[$varName];
344✔
503
        }
504

505
        /**
506
         * Record that a variable has been defined and assigned a value.
507
         *
508
         * If a variable has been defined within a scope, it will not be marked as
509
         * undefined when that variable is later used. If it is not used, it will be
510
         * marked as unused when that scope ends.
511
         *
512
         * Sometimes it's possible to assign something to a variable without
513
         * definining it (eg: assignment to a reference); in that case, use
514
         * `markVariableAssignmentWithoutInitialization()`.
515
         *
516
         * @param string $varName
517
         * @param int    $stackPtr
518
         * @param int    $currScope
519
         *
520
         * @return void
521
         */
522
        protected function markVariableAssignment($varName, $stackPtr, $currScope)
328✔
523
        {
524
                Helpers::debug('markVariableAssignment: starting for', $varName);
328✔
525
                $this->markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope);
328✔
526
                Helpers::debug('markVariableAssignment: marked as assigned without initialization', $varName);
328✔
527
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
328✔
528
                if (isset($varInfo->firstInitialized) && ($varInfo->firstInitialized <= $stackPtr)) {
328✔
529
                        Helpers::debug('markVariableAssignment: variable is already initialized', $varName);
136✔
530
                        return;
136✔
531
                }
532
                $varInfo->firstInitialized = $stackPtr;
328✔
533
                Helpers::debug('markVariableAssignment: marked as initialized', $varName);
328✔
534
        }
164✔
535

536
        /**
537
         * Record that a variable has been assigned a value.
538
         *
539
         * Does not record that a variable has been defined, which is the usual state
540
         * of affairs. For that, use `markVariableAssignment()`.
541
         *
542
         * This is useful for assignments to references.
543
         *
544
         * @param string $varName
545
         * @param int    $stackPtr
546
         * @param int    $currScope
547
         *
548
         * @return void
549
         */
550
        protected function markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope)
328✔
551
        {
552
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
328✔
553

554
                // Is the variable referencing another variable? If so, mark that variable used also.
555
                if ($varInfo->referencedVariableScope !== null && $varInfo->referencedVariableScope !== $currScope) {
328✔
556
                        Helpers::debug('markVariableAssignmentWithoutInitialization: considering marking referenced variable assigned', $varName);
24✔
557
                        // Don't do this if the referenced variable does not exist; eg: if it's going to be bound at runtime like in array_walk
558
                        if ($this->getVariableInfo($varInfo->name, $varInfo->referencedVariableScope)) {
24✔
559
                                Helpers::debug('markVariableAssignmentWithoutInitialization: marking referenced variable as assigned also', $varName);
4✔
560
                                $this->markVariableAssignment($varInfo->name, $stackPtr, $varInfo->referencedVariableScope);
4✔
561
                        } else {
1✔
562
                                Helpers::debug('markVariableAssignmentWithoutInitialization: not marking referenced variable assigned', $varName);
22✔
563
                        }
564
                } else {
6✔
565
                                Helpers::debug('markVariableAssignmentWithoutInitialization: not considering referenced variable', $varName);
328✔
566
                }
567

568
                if (empty($varInfo->scopeType)) {
328✔
569
                        $varInfo->scopeType = ScopeType::LOCAL;
320✔
570
                }
80✔
571
                $varInfo->allAssignments[] = $stackPtr;
328✔
572
        }
164✔
573

574
        /**
575
         * Record that a variable has been defined within a scope.
576
         *
577
         * @param string                                                                                           $varName
578
         * @param ScopeType::PARAM|ScopeType::BOUND|ScopeType::LOCAL|ScopeType::GLOBALSCOPE|ScopeType::STATICSCOPE $scopeType
579
         * @param ?string                                                                                          $typeHint
580
         * @param int                                                                                              $stackPtr
581
         * @param int                                                                                              $currScope
582
         * @param ?bool                                                                                            $permitMatchingRedeclaration
583
         *
584
         * @return void
585
         */
586
        protected function markVariableDeclaration(
260✔
587
                $varName,
588
                $scopeType,
589
                $typeHint,
590
                $stackPtr,
591
                $currScope,
592
                $permitMatchingRedeclaration = false
593
        ) {
594
                Helpers::debug("marking variable '{$varName}' declared in scope starting at token", $currScope);
260✔
595
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
260✔
596

597
                if (! empty($varInfo->scopeType)) {
260✔
598
                        if (($permitMatchingRedeclaration === false) || ($varInfo->scopeType !== $scopeType)) {
16✔
599
                                //  Issue redeclaration/reuse warning
600
                                //  Note: we check off scopeType not firstDeclared, this is so that
601
                                //    we catch declarations that come after implicit declarations like
602
                                //    use of a variable as a local.
603
                                $this->addWarning(
8✔
604
                                        'Redeclaration of %s %s as %s.',
8✔
605
                                        $stackPtr,
8✔
606
                                        'VariableRedeclaration',
8✔
607
                                        [
4✔
608
                                                VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType],
8✔
609
                                                "\${$varName}",
8✔
610
                                                VariableInfo::$scopeTypeDescriptions[$scopeType],
8✔
611
                                        ]
4✔
612
                                );
6✔
613
                        }
2✔
614
                }
4✔
615

616
                $varInfo->scopeType = $scopeType;
260✔
617
                if (isset($typeHint)) {
260✔
618
                        $varInfo->typeHint = $typeHint;
×
619
                }
620
                if (isset($varInfo->firstDeclared) && ($varInfo->firstDeclared <= $stackPtr)) {
260✔
621
                        Helpers::debug("variable '{$varName}' was already marked declared", $varInfo);
16✔
622
                        return;
16✔
623
                }
624
                $varInfo->firstDeclared = $stackPtr;
260✔
625
                $varInfo->allAssignments[] = $stackPtr;
260✔
626
                Helpers::debug("variable '{$varName}' marked declared", $varInfo);
260✔
627
        }
130✔
628

629
        /**
630
         * @param string   $message
631
         * @param int      $stackPtr
632
         * @param string   $code
633
         * @param string[] $data
634
         *
635
         * @return void
636
         */
637
        protected function addWarning($message, $stackPtr, $code, $data)
8✔
638
        {
639
                if (! $this->currentFile) {
8✔
640
                        throw new \Exception('Cannot add warning; current file is not set.');
×
641
                }
642
                $this->currentFile->addWarning(
8✔
643
                        $message,
8✔
644
                        $stackPtr,
8✔
645
                        $code,
8✔
646
                        $data
6✔
647
                );
6✔
648
        }
4✔
649

650
        /**
651
         * Record that a variable has been used within a scope.
652
         *
653
         * If the variable has not been defined first, this will still mark it used.
654
         * To display a warning for undefined variables, use
655
         * `markVariableReadAndWarnIfUndefined()`.
656
         *
657
         * @param string $varName
658
         * @param int    $stackPtr
659
         * @param int    $currScope
660
         *
661
         * @return void
662
         */
663
        protected function markVariableRead($varName, $stackPtr, $currScope)
340✔
664
        {
665
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
340✔
666
                if (isset($varInfo->firstRead) && ($varInfo->firstRead <= $stackPtr)) {
340✔
667
                        return;
216✔
668
                }
669
                $varInfo->firstRead = $stackPtr;
340✔
670
        }
170✔
671

672
        /**
673
         * Return true if a variable is defined within a scope.
674
         *
675
         * @param string $varName
676
         * @param int    $stackPtr
677
         * @param int    $currScope
678
         *
679
         * @return bool
680
         */
681
        protected function isVariableUndefined($varName, $stackPtr, $currScope)
340✔
682
        {
683
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
340✔
684
                Helpers::debug('isVariableUndefined', $varInfo, 'at', $stackPtr);
340✔
685
                if ($varInfo->ignoreUndefined) {
340✔
686
                        return false;
24✔
687
                }
688
                if (isset($varInfo->firstDeclared) && $varInfo->firstDeclared <= $stackPtr) {
340✔
689
                        return false;
232✔
690
                }
691
                if (isset($varInfo->firstInitialized) && $varInfo->firstInitialized <= $stackPtr) {
332✔
692
                        return false;
300✔
693
                }
694
                // If we are inside a for loop increment expression, check to see if the
695
                // variable was defined inside the for loop.
696
                foreach ($this->forLoops as $forLoop) {
260✔
697
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
698
                                Helpers::debug('isVariableUndefined looking at increment expression for loop', $forLoop);
8✔
699
                                if (
700
                                        isset($varInfo->firstInitialized)
8✔
701
                                        && $varInfo->firstInitialized > $forLoop->blockStart
8✔
702
                                        && $varInfo->firstInitialized < $forLoop->blockEnd
8✔
703
                                ) {
2✔
704
                                        return false;
8✔
705
                                }
706
                        }
2✔
707
                }
65✔
708
                // If we are inside a for loop body, check to see if the variable was
709
                // defined in that loop's third expression.
710
                foreach ($this->forLoops as $forLoop) {
260✔
711
                        if ($stackPtr > $forLoop->blockStart && $stackPtr < $forLoop->blockEnd) {
8✔
712
                                foreach ($forLoop->incrementVariables as $forLoopVarInfo) {
8✔
713
                                        if ($varInfo === $forLoopVarInfo) {
8✔
714
                                                return false;
×
715
                                        }
716
                                }
2✔
717
                        }
2✔
718
                }
65✔
719
                return true;
260✔
720
        }
721

722
        /**
723
         * Record a variable use and report a warning if the variable is undefined.
724
         *
725
         * @param File   $phpcsFile
726
         * @param string $varName
727
         * @param int    $stackPtr
728
         * @param int    $currScope
729
         *
730
         * @return void
731
         */
732
        protected function markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope)
340✔
733
        {
734
                $this->markVariableRead($varName, $stackPtr, $currScope);
340✔
735
                if ($this->isVariableUndefined($varName, $stackPtr, $currScope) === true) {
340✔
736
                        Helpers::debug("variable $varName looks undefined");
260✔
737

738
                        if (Helpers::isVariableArrayPushShortcut($phpcsFile, $stackPtr)) {
260✔
739
                                $this->warnAboutUndefinedArrayPushShortcut($phpcsFile, $varName, $stackPtr);
28✔
740
                                // Mark the variable as defined if it's of the form `$x[] = 1;`
741
                                $this->markVariableAssignment($varName, $stackPtr, $currScope);
28✔
742
                                return;
28✔
743
                        }
744

745
                        if (Helpers::isVariableInsideUnset($phpcsFile, $stackPtr)) {
260✔
746
                                $this->warnAboutUndefinedUnset($phpcsFile, $varName, $stackPtr);
8✔
747
                                return;
8✔
748
                        }
749

750
                        $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
252✔
751
                }
63✔
752
        }
170✔
753

754
        /**
755
         * Mark all variables within a scope as being used.
756
         *
757
         * This will prevent any of the variables in that scope from being reported
758
         * as unused.
759
         *
760
         * @param File $phpcsFile
761
         * @param int  $stackPtr
762
         *
763
         * @return void
764
         */
765
        protected function markAllVariablesRead(File $phpcsFile, $stackPtr)
4✔
766
        {
767
                $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr);
4✔
768
                if ($currScope === null) {
4✔
769
                        return;
×
770
                }
771
                $scopeInfo = $this->getOrCreateScopeInfo($currScope);
4✔
772
                $count = count($scopeInfo->variables);
4✔
773
                Helpers::debug("marking all $count variables in scope as read");
4✔
774
                foreach ($scopeInfo->variables as $varInfo) {
4✔
775
                        $this->markVariableRead($varInfo->name, $stackPtr, $scopeInfo->scopeStartIndex);
4✔
776
                }
1✔
777
        }
2✔
778

779
        /**
780
         * Process a parameter definition if it is inside a function definition.
781
         *
782
         * This does not include variables imported by a "use" statement.
783
         *
784
         * @param File   $phpcsFile
785
         * @param int    $stackPtr
786
         * @param string $varName
787
         * @param int    $outerScope
788
         *
789
         * @return void
790
         */
791
        protected function processVariableAsFunctionParameter(File $phpcsFile, $stackPtr, $varName, $outerScope)
232✔
792
        {
793
                Helpers::debug('processVariableAsFunctionParameter', $stackPtr, $varName);
232✔
794
                $tokens = $phpcsFile->getTokens();
232✔
795

796
                $functionPtr = Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
232✔
797
                if (! is_int($functionPtr)) {
232✔
798
                        throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
799
                }
800

801
                Helpers::debug('processVariableAsFunctionParameter found function definition', $tokens[$functionPtr]);
232✔
802
                $this->markVariableDeclaration($varName, ScopeType::PARAM, null, $stackPtr, $functionPtr);
232✔
803

804
                // Are we pass-by-reference?
805
                $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
232✔
806
                if (($referencePtr !== false) && ($tokens[$referencePtr]['code'] === T_BITWISE_AND)) {
232✔
807
                        Helpers::debug('processVariableAsFunctionParameter found pass-by-reference to scope', $outerScope);
32✔
808
                        $varInfo = $this->getOrCreateVariableInfo($varName, $functionPtr);
32✔
809
                        $varInfo->referencedVariableScope = $outerScope;
32✔
810
                }
8✔
811

812
                //  Are we optional with a default?
813
                if (Helpers::getNextAssignPointer($phpcsFile, $stackPtr) !== null) {
232✔
814
                        Helpers::debug('processVariableAsFunctionParameter optional with default');
16✔
815
                        $this->markVariableAssignment($varName, $stackPtr, $functionPtr);
16✔
816
                }
4✔
817

818
                // Are we using constructor promotion? If so, that counts as both definition and use.
819
                if (Helpers::isConstructorPromotion($phpcsFile, $stackPtr)) {
232✔
820
                        Helpers::debug('processVariableAsFunctionParameter constructor promotion');
12✔
821
                        $this->markVariableRead($varName, $stackPtr, $outerScope);
12✔
822
                }
3✔
823
        }
116✔
824

825
        /**
826
         * Process a variable definition if it is inside a function's "use" import.
827
         *
828
         * @param File   $phpcsFile
829
         * @param int    $stackPtr
830
         * @param string $varName
831
         * @param int    $outerScope The start of the scope outside the function definition
832
         *
833
         * @return void
834
         */
835
        protected function processVariableAsUseImportDefinition(File $phpcsFile, $stackPtr, $varName, $outerScope)
24✔
836
        {
837
                $tokens = $phpcsFile->getTokens();
24✔
838

839
                Helpers::debug('processVariableAsUseImportDefinition', $stackPtr, $varName, $outerScope);
24✔
840

841
                $endOfArgsPtr = $phpcsFile->findPrevious([T_CLOSE_PARENTHESIS], $stackPtr - 1, null);
24✔
842
                if (! is_int($endOfArgsPtr)) {
24✔
843
                        throw new \Exception("Arguments index not found for function use index {$stackPtr} when processing variable {$varName}");
×
844
                }
845
                $functionPtr = Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $endOfArgsPtr);
24✔
846
                if (! is_int($functionPtr)) {
24✔
847
                        throw new \Exception("Function index not found for function use index {$stackPtr} (using {$endOfArgsPtr}) when processing variable {$varName}");
×
848
                }
849

850
                // Use is both a read (in the enclosing scope) and a define (in the function scope)
851
                $this->markVariableRead($varName, $stackPtr, $outerScope);
24✔
852

853
                // If it's undefined in the enclosing scope, the use is wrong
854
                if ($this->isVariableUndefined($varName, $stackPtr, $outerScope) === true) {
24✔
855
                        Helpers::debug("variable '{$varName}' in function definition looks undefined in scope", $outerScope);
8✔
856
                        $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
8✔
857
                        return;
8✔
858
                }
859

860
                $this->markVariableDeclaration($varName, ScopeType::BOUND, null, $stackPtr, $functionPtr);
24✔
861
                $this->markVariableAssignment($varName, $stackPtr, $functionPtr);
24✔
862

863
                // Are we pass-by-reference? If so, then any assignment to the variable in
864
                // the function scope also should count for the enclosing scope.
865
                $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
24✔
866
                if (is_int($referencePtr) && $tokens[$referencePtr]['code'] === T_BITWISE_AND) {
24✔
867
                        Helpers::debug("variable '{$varName}' in function definition looks passed by reference");
12✔
868
                        $varInfo = $this->getOrCreateVariableInfo($varName, $functionPtr);
12✔
869
                        $varInfo->referencedVariableScope = $outerScope;
12✔
870
                }
3✔
871
        }
12✔
872

873
        /**
874
         * Process a class property that is being defined.
875
         *
876
         * Property definitions are ignored currently because all property access is
877
         * legal, even to undefined properties.
878
         *
879
         * Can be called for any token and will return false if the variable is not
880
         * of this type.
881
         *
882
         * @param File $phpcsFile
883
         * @param int  $stackPtr
884
         *
885
         * @return bool
886
         */
887
        protected function processVariableAsClassProperty(File $phpcsFile, $stackPtr)
344✔
888
        {
889
                // Make sure we are not in a class method before assuming it's a property.
890
                $tokens = $phpcsFile->getTokens();
344✔
891

892
                /** @var array{conditions?: (int|string)[], content?: string}|null */
893
                $token = $tokens[$stackPtr];
344✔
894
                if ($token && !empty($token['conditions']) && !empty($token['content']) && !Helpers::areConditionsWithinFunctionBeforeClass($token)) {
344✔
895
                        return Helpers::areAnyConditionsAClass($token);
16✔
896
                }
897
                return false;
344✔
898
        }
899

900
        /**
901
         * Process a variable that is being accessed inside a catch block.
902
         *
903
         * Can be called for any token and will return false if the variable is not
904
         * of this type.
905
         *
906
         * @param File   $phpcsFile
907
         * @param int    $stackPtr
908
         * @param string $varName
909
         * @param int    $currScope
910
         *
911
         * @return bool
912
         */
913
        protected function processVariableAsCatchBlock(File $phpcsFile, $stackPtr, $varName, $currScope)
352✔
914
        {
915
                $tokens = $phpcsFile->getTokens();
352✔
916

917
                // Are we a catch block parameter?
918
                $openPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
352✔
919
                if ($openPtr === null) {
352✔
920
                        return false;
348✔
921
                }
922

923
                $catchPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
260✔
924
                if (($catchPtr !== false) && ($tokens[$catchPtr]['code'] === T_CATCH)) {
260✔
925
                        // Scope of the exception var is actually the function, not just the catch block.
926
                        $this->markVariableDeclaration($varName, ScopeType::LOCAL, null, $stackPtr, $currScope, true);
44✔
927
                        $this->markVariableAssignment($varName, $stackPtr, $currScope);
44✔
928
                        if ($this->allowUnusedCaughtExceptions) {
44✔
929
                                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
40✔
930
                                $varInfo->ignoreUnused = true;
40✔
931
                        }
10✔
932
                        return true;
44✔
933
                }
934
                return false;
216✔
935
        }
936

937
        /**
938
         * Process a variable that is being accessed as a member of `$this`.
939
         *
940
         * Looks for variables of the form `$this->myVariable`.
941
         *
942
         * Can be called for any token and will return false if the variable is not
943
         * of this type.
944
         *
945
         * @param File   $phpcsFile
946
         * @param int    $stackPtr
947
         * @param string $varName
948
         *
949
         * @return bool
950
         */
951
        protected function processVariableAsThisWithinClass(File $phpcsFile, $stackPtr, $varName)
352✔
952
        {
953
                $tokens = $phpcsFile->getTokens();
352✔
954
                $token  = $tokens[$stackPtr];
352✔
955

956
                // Are we $this within a class?
957
                if (($varName !== 'this') || empty($token['conditions'])) {
352✔
958
                        return false;
336✔
959
                }
960

961
                // Handle enums specially since their condition may not exist in old phpcs.
962
                $inEnum = false;
80✔
963
                foreach ($this->enums as $enum) {
80✔
964
                        if ($stackPtr > $enum->blockStart && $stackPtr < $enum->blockEnd) {
4✔
965
                                $inEnum = true;
4✔
966
                        }
1✔
967
                }
20✔
968

969
                $inFunction = false;
80✔
970
                foreach (array_reverse($token['conditions'], true) as $scopeCode) {
80✔
971
                        //  $this within a closure is valid
972
                        if ($scopeCode === T_CLOSURE && $inFunction === false) {
80✔
973
                                return true;
12✔
974
                        }
975

976
                        $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
80✔
977
                        if (defined('T_ENUM')) {
80✔
978
                                $classlikeCodes[] = T_ENUM;
80✔
979
                        }
20✔
980
                        if (in_array($scopeCode, $classlikeCodes, true)) {
80✔
981
                                return true;
72✔
982
                        }
983

984
                        if ($scopeCode === T_FUNCTION && $inEnum) {
80✔
985
                                return true;
4✔
986
                        }
987

988
                        // Handle nested function declarations.
989
                        if ($scopeCode === T_FUNCTION) {
80✔
990
                                if ($inFunction === true) {
80✔
991
                                        break;
4✔
992
                                }
993

994
                                $inFunction = true;
80✔
995
                        }
20✔
996
                }
20✔
997

998
                return false;
12✔
999
        }
1000

1001
        /**
1002
         * Process a superglobal variable that is being accessed.
1003
         *
1004
         * Can be called for any token and will return false if the variable is not
1005
         * of this type.
1006
         *
1007
         * @param string $varName
1008
         *
1009
         * @return bool
1010
         */
1011
        protected function processVariableAsSuperGlobal($varName)
344✔
1012
        {
1013
                $superglobals = [
172✔
1014
                        'GLOBALS',
344✔
1015
                        '_SERVER',
258✔
1016
                        '_GET',
258✔
1017
                        '_POST',
258✔
1018
                        '_FILES',
258✔
1019
                        '_COOKIE',
258✔
1020
                        '_SESSION',
258✔
1021
                        '_REQUEST',
258✔
1022
                        '_ENV',
258✔
1023
                        'argv',
258✔
1024
                        'argc',
258✔
1025
                        'http_response_header',
258✔
1026
                        'HTTP_RAW_POST_DATA',
258✔
1027
                ];
258✔
1028
                // Are we a superglobal variable?
1029
                return (in_array($varName, $superglobals, true));
344✔
1030
        }
1031

1032
        /**
1033
         * Process a variable that is being accessed with static syntax.
1034
         *
1035
         * That is, this will record the use of a variable of the form
1036
         * `MyClass::$myVariable` or `self::$myVariable`.
1037
         *
1038
         * Can be called for any token and will return false if the variable is not
1039
         * of this type.
1040
         *
1041
         * @param File $phpcsFile
1042
         * @param int  $stackPtr
1043
         *
1044
         * @return bool
1045
         */
1046
        protected function processVariableAsStaticMember(File $phpcsFile, $stackPtr)
344✔
1047
        {
1048
                $tokens = $phpcsFile->getTokens();
344✔
1049

1050
                $doubleColonPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
344✔
1051
                if ($doubleColonPtr === false || $tokens[$doubleColonPtr]['code'] !== T_DOUBLE_COLON) {
344✔
1052
                        return false;
344✔
1053
                }
1054
                $classNamePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $doubleColonPtr - 1, null, true);
28✔
1055
                $staticReferences = [
14✔
1056
                        T_STRING,
28✔
1057
                        T_SELF,
28✔
1058
                        T_PARENT,
28✔
1059
                        T_STATIC,
28✔
1060
                        T_VARIABLE,
28✔
1061
                ];
21✔
1062
                if ($classNamePtr === false || ! in_array($tokens[$classNamePtr]['code'], $staticReferences, true)) {
28✔
1063
                        return false;
×
1064
                }
1065
                // "When calling static methods, the function call is stronger than the
1066
                // static property operator" so look for a function call.
1067
                $parenPointer = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
28✔
1068
                if ($parenPointer !== false && $tokens[$parenPointer]['code'] === T_OPEN_PARENTHESIS) {
28✔
1069
                        return false;
4✔
1070
                }
1071
                return true;
28✔
1072
        }
1073

1074
        /**
1075
         * @param File   $phpcsFile
1076
         * @param int    $stackPtr
1077
         * @param string $varName
1078
         *
1079
         * @return bool
1080
         */
1081
        protected function processVariableAsStaticOutsideClass(File $phpcsFile, $stackPtr, $varName)
344✔
1082
        {
1083
                // Are we refering to self:: outside a class?
1084

1085
                $tokens = $phpcsFile->getTokens();
344✔
1086

1087
                /** @var array{conditions?: (int|string)[], content?: string}|null */
1088
                $token = $tokens[$stackPtr];
344✔
1089

1090
                $doubleColonPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
344✔
1091
                if ($doubleColonPtr === false || $tokens[$doubleColonPtr]['code'] !== T_DOUBLE_COLON) {
344✔
1092
                        return false;
344✔
1093
                }
1094
                $classNamePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $doubleColonPtr - 1, null, true);
28✔
1095
                if ($classNamePtr === false) {
28✔
1096
                        return false;
×
1097
                }
1098
                $code = $tokens[$classNamePtr]['code'];
28✔
1099
                $staticReferences = [
14✔
1100
                        T_SELF,
28✔
1101
                        T_STATIC,
28✔
1102
                ];
21✔
1103
                if (! in_array($code, $staticReferences, true)) {
28✔
1104
                        return false;
16✔
1105
                }
1106
                $errorClass = $code === T_SELF ? 'SelfOutsideClass' : 'StaticOutsideClass';
28✔
1107
                $staticRefType = $code === T_SELF ? 'self::' : 'static::';
28✔
1108
                if (!empty($token['conditions']) && !empty($token['content']) && Helpers::areAnyConditionsAClass($token)) {
28✔
1109
                        return false;
28✔
1110
                }
1111
                $phpcsFile->addError(
8✔
1112
                        "Use of {$staticRefType}%s outside class definition.",
8✔
1113
                        $stackPtr,
8✔
1114
                        $errorClass,
8✔
1115
                        ["\${$varName}"]
8✔
1116
                );
6✔
1117
                return true;
8✔
1118
        }
1119

1120
        /**
1121
         * Process a variable that is being assigned.
1122
         *
1123
         * This will record that the variable has been defined within a scope so that
1124
         * later we can determine if it it unused and we can guarantee that any
1125
         * future uses of the variable are not using an undefined variable.
1126
         *
1127
         * References (on either side of an assignment) behave differently and this
1128
         * function handles those cases as well.
1129
         *
1130
         * @param File   $phpcsFile
1131
         * @param int    $stackPtr
1132
         * @param string $varName
1133
         * @param int    $currScope
1134
         *
1135
         * @return void
1136
         */
1137
        protected function processVariableAsAssignment(File $phpcsFile, $stackPtr, $varName, $currScope)
324✔
1138
        {
1139
                Helpers::debug("processVariableAsAssignment: starting for '{$varName}'");
324✔
1140
                $assignPtr = Helpers::getNextAssignPointer($phpcsFile, $stackPtr);
324✔
1141
                if (! is_int($assignPtr)) {
324✔
1142
                        return;
×
1143
                }
1144

1145
                // If the right-hand-side of the assignment to this variable is a reference
1146
                // variable, then this variable is a reference to that one, and as such any
1147
                // assignment to this variable (except another assignment by reference,
1148
                // which would change the binding) has a side effect of changing the
1149
                // referenced variable and therefore should count as both an assignment and
1150
                // a read.
1151
                $tokens = $phpcsFile->getTokens();
324✔
1152
                $referencePtr = $phpcsFile->findNext(Tokens::$emptyTokens, $assignPtr + 1, null, true, null, true);
324✔
1153
                if (is_int($referencePtr) && $tokens[$referencePtr]['code'] === T_BITWISE_AND) {
324✔
1154
                        Helpers::debug("processVariableAsAssignment: found reference variable for '{$varName}'");
8✔
1155
                        $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
8✔
1156
                        // If the variable was already declared, but was not yet read, it is
1157
                        // unused because we're about to change the binding; that is, unless we
1158
                        // are inside a conditional block because in that case the condition may
1159
                        // never activate.
1160
                        $scopeInfo = $this->getOrCreateScopeInfo($currScope);
8✔
1161
                        $conditionPointer = Helpers::getClosestConditionPositionIfBeforeOtherConditions($tokens[$referencePtr]['conditions']);
8✔
1162
                        $lastAssignmentPtr = $varInfo->firstDeclared;
8✔
1163
                        if (! $conditionPointer && $lastAssignmentPtr) {
8✔
1164
                                Helpers::debug("processVariableAsAssignment: considering close of scope for '{$varName}' due to reference reassignment");
8✔
1165
                                $this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
8✔
1166
                        }
2✔
1167
                        if ($conditionPointer && $lastAssignmentPtr && $conditionPointer < $lastAssignmentPtr) {
8✔
1168
                                // We may be inside a condition but the last assignment was also inside this condition.
1169
                                Helpers::debug("processVariableAsAssignment: considering close of scope for '{$varName}' due to reference reassignment ignoring recent condition");
8✔
1170
                                $this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
8✔
1171
                        }
2✔
1172
                        if ($conditionPointer && $lastAssignmentPtr && $conditionPointer > $lastAssignmentPtr) {
8✔
1173
                                Helpers::debug("processVariableAsAssignment: not considering close of scope for '{$varName}' due to reference reassignment because it is conditional");
8✔
1174
                        }
2✔
1175
                        // The referenced variable may have a different name, but we don't
1176
                        // actually need to mark it as used in this case because the act of this
1177
                        // assignment will mark it used on the next token.
1178
                        $varInfo->referencedVariableScope = $currScope;
8✔
1179
                        $this->markVariableDeclaration($varName, ScopeType::LOCAL, null, $stackPtr, $currScope, true);
8✔
1180
                        // An assignment to a reference is a binding and should not count as
1181
                        // initialization since it doesn't change any values.
1182
                        $this->markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope);
8✔
1183
                        return;
8✔
1184
                }
1185

1186
                Helpers::debug('processVariableAsAssignment: marking as assignment in scope', $currScope);
324✔
1187
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
324✔
1188

1189
                // If the left-hand-side of the assignment (the variable we are examining)
1190
                // is itself a reference, then that counts as a read as well as a write.
1191
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
324✔
1192
                if ($varInfo->isDynamicReference) {
324✔
1193
                        Helpers::debug('processVariableAsAssignment: also marking as a use because variable is a reference');
20✔
1194
                        $this->markVariableRead($varName, $stackPtr, $currScope);
20✔
1195
                }
5✔
1196
        }
162✔
1197

1198
        /**
1199
         * Processes variables destructured from an array using shorthand list assignment.
1200
         *
1201
         * This will record the definition and assignment of variables defined using
1202
         * the format:
1203
         *
1204
         * ```
1205
         * [ $foo, $bar, $baz ] = $ary;
1206
         * ```
1207
         *
1208
         * Can be called for any token and will return false if the variable is not
1209
         * of this type.
1210
         *
1211
         * @param File   $phpcsFile
1212
         * @param int    $stackPtr
1213
         * @param string $varName
1214
         * @param int    $currScope
1215
         *
1216
         * @return bool
1217
         */
1218
        protected function processVariableAsListShorthandAssignment(File $phpcsFile, $stackPtr, $varName, $currScope)
336✔
1219
        {
1220
                // OK, are we within a [ ... ] construct?
1221
                $openPtr = Helpers::findContainingOpeningSquareBracket($phpcsFile, $stackPtr);
336✔
1222
                if (! is_int($openPtr)) {
336✔
1223
                        return false;
336✔
1224
                }
1225

1226
                // OK, we're a [ ... ] construct... are we being assigned to?
1227
                $assignments = Helpers::getListAssignments($phpcsFile, $openPtr);
48✔
1228
                if (! $assignments) {
48✔
1229
                        return false;
40✔
1230
                }
1231
                $matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) {
16✔
1232
                        if ($assignment === $stackPtr) {
32✔
1233
                                return $assignment;
32✔
1234
                        }
1235
                        return $thisAssignment;
24✔
1236
                });
32✔
1237
                if (! $matchingAssignment) {
32✔
1238
                        return false;
×
1239
                }
1240

1241
                // Yes, we're being assigned.
1242
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
32✔
1243
                return true;
32✔
1244
        }
1245

1246
        /**
1247
         * Processes variables destructured from an array using list assignment.
1248
         *
1249
         * This will record the definition and assignment of variables defined using
1250
         * the format:
1251
         *
1252
         * ```
1253
         * list( $foo, $bar, $baz ) = $ary;
1254
         * ```
1255
         *
1256
         * Can be called for any token and will return false if the variable is not
1257
         * of this type.
1258
         *
1259
         * @param File   $phpcsFile
1260
         * @param int    $stackPtr
1261
         * @param string $varName
1262
         * @param int    $currScope
1263
         *
1264
         * @return bool
1265
         */
1266
        protected function processVariableAsListAssignment(File $phpcsFile, $stackPtr, $varName, $currScope)
336✔
1267
        {
1268
                $tokens = $phpcsFile->getTokens();
336✔
1269

1270
                // OK, are we within a list (...) construct?
1271
                $openPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
336✔
1272
                if ($openPtr === null) {
336✔
1273
                        return false;
288✔
1274
                }
1275

1276
                $prevPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
216✔
1277
                if ((is_bool($prevPtr)) || ($tokens[$prevPtr]['code'] !== T_LIST)) {
216✔
1278
                        return false;
216✔
1279
                }
1280

1281
                // OK, we're a list (...) construct... are we being assigned to?
1282
                $assignments = Helpers::getListAssignments($phpcsFile, $prevPtr);
32✔
1283
                if (! $assignments) {
32✔
UNCOV
1284
                        return false;
×
1285
                }
1286
                $matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) {
24✔
1287
                        if ($assignment === $stackPtr) {
32✔
1288
                                return $assignment;
32✔
1289
                        }
1290
                        return $thisAssignment;
24✔
1291
                });
32✔
1292
                if (! $matchingAssignment) {
32✔
1293
                        return false;
×
1294
                }
1295

1296
                // Yes, we're being assigned.
1297
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
32✔
1298
                return true;
32✔
1299
        }
1300

1301
        /**
1302
         * Process a variable being defined (imported, really) with the `global` keyword.
1303
         *
1304
         * Can be called for any token and will return false if the variable is not
1305
         * of this type.
1306
         *
1307
         * @param File   $phpcsFile
1308
         * @param int    $stackPtr
1309
         * @param string $varName
1310
         * @param int    $currScope
1311
         *
1312
         * @return bool
1313
         */
1314
        protected function processVariableAsGlobalDeclaration(File $phpcsFile, $stackPtr, $varName, $currScope)
336✔
1315
        {
1316
                $tokens = $phpcsFile->getTokens();
336✔
1317

1318
                // Are we a global declaration?
1319
                // Search backwards for first token that isn't whitespace/comment, comma or variable.
1320
                $ignore             = Tokens::$emptyTokens;
336✔
1321
                $ignore[T_VARIABLE] = T_VARIABLE;
336✔
1322
                $ignore[T_COMMA]    = T_COMMA;
336✔
1323

1324
                $globalPtr = $phpcsFile->findPrevious($ignore, $stackPtr - 1, null, true, null, true);
336✔
1325
                if (($globalPtr === false) || ($tokens[$globalPtr]['code'] !== T_GLOBAL)) {
336✔
1326
                        return false;
336✔
1327
                }
1328

1329
                // It's a global declaration.
1330
                $this->markVariableDeclaration($varName, ScopeType::GLOBALSCOPE, null, $stackPtr, $currScope);
36✔
1331
                return true;
36✔
1332
        }
1333

1334
        /**
1335
         * Process a variable as a static declaration within a function.
1336
         *
1337
         * Specifically, this looks for variable definitions of the form `static
1338
         * $foo = 'hello';` or `static int $foo;` inside a function definition.
1339
         *
1340
         * This will not operate on variables that are written in a class definition
1341
         * outside of a function like `static $foo;` or `public static ?int $foo =
1342
         * 'bar';` because class properties (static or instance) are currently not
1343
         * tracked by this sniff. This is because a class property might be unused
1344
         * inside the class, but used outside the class (we cannot easily know if it
1345
         * is unused); this is also because it's common and legal to define class
1346
         * properties when they are assigned and that assignment can happen outside a
1347
         * class (we cannot easily know if the use of a property is undefined). These
1348
         * sorts of checks are better performed by static analysis tools that can see
1349
         * a whole project rather than a linter which can only easily see a file or
1350
         * some lines.
1351
         *
1352
         * If found, such a variable will be marked as declared (and possibly
1353
         * assigned, if it includes an initial value) within the scope of the
1354
         * function body.
1355
         *
1356
         * This will not operate on variables that use late static binding
1357
         * (`static::$foobar`) or the parameters of static methods even though they
1358
         * include the word `static` in the same statement.
1359
         *
1360
         * This only finds the defintions of static variables. Their use is handled
1361
         * by `processVariableAsStaticMember()`.
1362
         *
1363
         * Can be called for any token and will return false if the variable is not
1364
         * of this type.
1365
         *
1366
         * @param File   $phpcsFile
1367
         * @param int    $stackPtr
1368
         * @param string $varName
1369
         * @param int    $currScope
1370
         *
1371
         * @return bool
1372
         */
1373
        protected function processVariableAsStaticDeclaration(File $phpcsFile, $stackPtr, $varName, $currScope)
336✔
1374
        {
1375
                $tokens = $phpcsFile->getTokens();
336✔
1376

1377
                // Search backwards for a `static` keyword that occurs before the start of the statement.
1378
                $startOfStatement = $phpcsFile->findPrevious([T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_FN_ARROW, T_OPEN_PARENTHESIS], $stackPtr - 1, null, false, null, true);
336✔
1379
                $staticPtr = $phpcsFile->findPrevious([T_STATIC], $stackPtr - 1, null, false, null, true);
336✔
1380
                if (! is_int($startOfStatement)) {
336✔
1381
                        $startOfStatement = 1;
12✔
1382
                }
3✔
1383
                if (! is_int($staticPtr)) {
336✔
1384
                        return false;
332✔
1385
                }
1386
                // PHPCS is bad at finding the start of statements so we have to do it ourselves.
1387
                if ($staticPtr < $startOfStatement) {
64✔
1388
                        return false;
32✔
1389
                }
1390

1391
                // Is the 'static' keyword an anonymous static function declaration? If so,
1392
                // this is not a static variable declaration.
1393
                $tokenAfterStatic = $phpcsFile->findNext(Tokens::$emptyTokens, $staticPtr + 1, null, true, null, true);
64✔
1394
                $functionTokenTypes = [
32✔
1395
                        T_FUNCTION,
64✔
1396
                        T_CLOSURE,
64✔
1397
                        T_FN,
64✔
1398
                ];
48✔
1399
                if (is_int($tokenAfterStatic) && in_array($tokens[$tokenAfterStatic]['code'], $functionTokenTypes, true)) {
64✔
1400
                        return false;
16✔
1401
                }
1402

1403
                // Is the token inside function parameters? If so, this is not a static
1404
                // declaration because we must be inside a function body.
1405
                if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
56✔
1406
                        return false;
×
1407
                }
1408

1409
                // Is the token inside a function call? If so, this is not a static
1410
                // declaration.
1411
                if (Helpers::isTokenInsideFunctionCallArgument($phpcsFile, $stackPtr)) {
56✔
1412
                        return false;
12✔
1413
                }
1414

1415
                // Is the keyword a late static binding? If so, this isn't the static
1416
                // keyword we're looking for, but since static:: isn't allowed in a
1417
                // compile-time constant, we also know we can't be part of a static
1418
                // declaration anyway, so there's no need to look any further.
1419
                $lateStaticBindingPtr = $phpcsFile->findNext(T_WHITESPACE, $staticPtr + 1, null, true, null, true);
44✔
1420
                if (($lateStaticBindingPtr !== false) && ($tokens[$lateStaticBindingPtr]['code'] === T_DOUBLE_COLON)) {
44✔
1421
                        return false;
4✔
1422
                }
1423

1424
                $this->markVariableDeclaration($varName, ScopeType::STATICSCOPE, null, $stackPtr, $currScope);
40✔
1425
                if (Helpers::getNextAssignPointer($phpcsFile, $stackPtr) !== null) {
40✔
1426
                        $this->markVariableAssignment($varName, $stackPtr, $currScope);
×
1427
                }
1428
                return true;
40✔
1429
        }
1430

1431
        /**
1432
         * @param File   $phpcsFile
1433
         * @param int    $stackPtr
1434
         * @param string $varName
1435
         * @param int    $currScope
1436
         *
1437
         * @return bool
1438
         */
1439
        protected function processVariableAsForeachLoopVar(File $phpcsFile, $stackPtr, $varName, $currScope)
332✔
1440
        {
1441
                $tokens = $phpcsFile->getTokens();
332✔
1442

1443
                // Are we a foreach loopvar?
1444
                $openParenPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
332✔
1445
                if (! is_int($openParenPtr)) {
332✔
1446
                        return false;
284✔
1447
                }
1448
                $foreachPtr = Helpers::getIntOrNull($phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true));
216✔
1449
                if (! is_int($foreachPtr)) {
216✔
1450
                        return false;
×
1451
                }
1452
                if ($tokens[$foreachPtr]['code'] === T_LIST) {
216✔
UNCOV
1453
                        $openParenPtr = Helpers::findContainingOpeningBracket($phpcsFile, $foreachPtr);
×
UNCOV
1454
                        if (! is_int($openParenPtr)) {
×
1455
                                return false;
×
1456
                        }
UNCOV
1457
                        $foreachPtr = Helpers::getIntOrNull($phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true));
×
UNCOV
1458
                        if (! is_int($foreachPtr)) {
×
1459
                                return false;
×
1460
                        }
1461
                }
1462
                if ($tokens[$foreachPtr]['code'] !== T_FOREACH) {
216✔
1463
                        return false;
188✔
1464
                }
1465

1466
                // Is there an 'as' token between us and the foreach?
1467
                if ($phpcsFile->findPrevious(T_AS, $stackPtr - 1, $openParenPtr) === false) {
64✔
1468
                        return false;
64✔
1469
                }
1470
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
64✔
1471
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
64✔
1472

1473
                // Is this the value of a key => value foreach?
1474
                if ($phpcsFile->findPrevious(T_DOUBLE_ARROW, $stackPtr - 1, $openParenPtr) !== false) {
64✔
1475
                        $varInfo->isForeachLoopAssociativeValue = true;
20✔
1476
                }
5✔
1477

1478
                // Are we pass-by-reference?
1479
                $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
64✔
1480
                if (($referencePtr !== false) && ($tokens[$referencePtr]['code'] === T_BITWISE_AND)) {
64✔
1481
                        Helpers::debug('processVariableAsForeachLoopVar: found foreach loop variable assigned by reference');
28✔
1482
                        $varInfo->isDynamicReference = true;
28✔
1483
                }
7✔
1484

1485
                return true;
64✔
1486
        }
1487

1488
        /**
1489
         * @param File   $phpcsFile
1490
         * @param int    $stackPtr
1491
         * @param string $varName
1492
         * @param int    $currScope
1493
         *
1494
         * @return bool
1495
         */
1496
        protected function processVariableAsPassByReferenceFunctionCall(File $phpcsFile, $stackPtr, $varName, $currScope)
332✔
1497
        {
1498
                $tokens = $phpcsFile->getTokens();
332✔
1499

1500
                // Are we pass-by-reference to known pass-by-reference function?
1501
                $functionPtr = Helpers::findFunctionCall($phpcsFile, $stackPtr);
332✔
1502
                if ($functionPtr === null || ! isset($tokens[$functionPtr])) {
332✔
1503
                        return false;
320✔
1504
                }
1505

1506
                // Is our function a known pass-by-reference function?
1507
                $functionName = $tokens[$functionPtr]['content'];
160✔
1508

1509
                // In PHPCS 4.x, T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED, and T_NAME_RELATIVE
1510
                // tokens contain the full namespaced name. Extract just the base name for the
1511
                // first check so that 'my_function' in the config can match '\My\Namespace\my_function'.
1512
                $functionBaseName = $functionName;
160✔
1513
                if ($tokens[$functionPtr]['code'] === T_NAME_FULLY_QUALIFIED) {
160✔
1514
                        $lastBackslashPos = strrpos($functionName, '\\');
10✔
1515
                        if ($lastBackslashPos !== false) {
10✔
1516
                                $functionBaseName = substr($functionName, $lastBackslashPos + 1);
10✔
1517
                        }
1518
                } elseif ($tokens[$functionPtr]['code'] === T_NAME_QUALIFIED) {
160✔
1519
                        $lastBackslashPos = strrpos($functionName, '\\');
×
1520
                        if ($lastBackslashPos !== false) {
×
1521
                                $functionBaseName = substr($functionName, $lastBackslashPos + 1);
×
1522
                        }
1523
                } elseif ($tokens[$functionPtr]['code'] === T_NAME_RELATIVE) {
160✔
1524
                        $lastBackslashPos = strrpos($functionName, '\\');
×
1525
                        if ($lastBackslashPos !== false) {
×
1526
                                $functionBaseName = substr($functionName, $lastBackslashPos + 1);
×
1527
                        }
1528
                }
1529

1530
                // Ensure we have a string (should always be true, but helps static analyzers).
1531
                if (! is_string($functionBaseName) || $functionBaseName === '') {
160✔
1532
                        return false;
×
1533
                }
1534

1535
                $refArgs = $this->getPassByReferenceFunction($functionBaseName);
160✔
1536
                if (! $refArgs) {
160✔
1537
                        // Check again with the fully namespaced function name.
1538
                        $functionName = Helpers::getFunctionNameWithNamespace($phpcsFile, $functionPtr);
156✔
1539
                        if (! $functionName) {
156✔
1540
                                return false;
×
1541
                        }
1542
                        $refArgs = $this->getPassByReferenceFunction($functionName);
156✔
1543
                        if (! $refArgs) {
156✔
1544
                                return false;
156✔
1545
                        }
1546
                }
1✔
1547

1548
                $argPtrs = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
24✔
1549

1550
                // We're within a function call arguments list, find which arg we are.
1551
                $argPos = false;
24✔
1552
                foreach ($argPtrs as $idx => $ptrs) {
24✔
1553
                        if (in_array($stackPtr, $ptrs)) {
24✔
1554
                                $argPos = $idx + 1;
24✔
1555
                                break;
24✔
1556
                        }
1557
                }
6✔
1558
                if ($argPos === false) {
24✔
1559
                        return false;
×
1560
                }
1561
                if (!in_array($argPos, $refArgs)) {
24✔
1562
                        // Our arg wasn't mentioned explicitly, are we after an elipsis catch-all?
1563
                        $elipsis = array_search('...', $refArgs);
24✔
1564
                        if ($elipsis === false) {
24✔
1565
                                return false;
24✔
1566
                        }
1567
                        $elipsis = (int)$elipsis;
20✔
1568
                        if ($argPos < $refArgs[$elipsis - 1]) {
20✔
1569
                                return false;
20✔
1570
                        }
1571
                }
5✔
1572

1573
                // Our argument position matches that of a pass-by-ref argument,
1574
                // check that we're the only part of the argument expression.
1575
                foreach ($argPtrs[$argPos - 1] as $ptr) {
20✔
1576
                        if ($ptr === $stackPtr) {
20✔
1577
                                continue;
20✔
1578
                        }
1579
                        if (isset(Tokens::$emptyTokens[$tokens[$ptr]['code']]) === false) {
20✔
1580
                                return false;
×
1581
                        }
1582
                }
5✔
1583

1584
                // Just us, we can mark it as a write.
1585
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
20✔
1586
                // It's a read as well for purposes of used-variables.
1587
                $this->markVariableRead($varName, $stackPtr, $currScope);
20✔
1588
                return true;
20✔
1589
        }
1590

1591
        /**
1592
         * @param File   $phpcsFile
1593
         * @param int    $stackPtr
1594
         * @param string $varName
1595
         * @param int    $currScope
1596
         *
1597
         * @return bool
1598
         */
1599
        protected function processVariableAsSymbolicObjectProperty(File $phpcsFile, $stackPtr, $varName, $currScope)
352✔
1600
        {
1601
                $tokens = $phpcsFile->getTokens();
352✔
1602

1603
                // Are we a symbolic object property/function derefeference?
1604
                // Search backwards for first token that isn't whitespace, is it a "->" operator?
1605
                $objectOperatorPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
352✔
1606
                if (($objectOperatorPtr === false) || ($tokens[$objectOperatorPtr]['code'] !== T_OBJECT_OPERATOR)) {
352✔
1607
                        return false;
352✔
1608
                }
1609

1610
                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
28✔
1611
                return true;
28✔
1612
        }
1613

1614
        /**
1615
         * Process a normal variable in the code.
1616
         *
1617
         * Most importantly, this function determines if the variable use is a "read"
1618
         * (using the variable for something) or a "write" (an assignment) or,
1619
         * sometimes, both at once.
1620
         *
1621
         * It also determines the scope of the variable (where it begins and ends).
1622
         *
1623
         * Using these two pieces of information, we can determine if the variable is
1624
         * being used ("read") without having been defined ("write").
1625
         *
1626
         * We can also determine, once the scan has hit the end of a scope, if any of
1627
         * the variables within that scope have been defined ("write") without being
1628
         * used ("read"). That behavior, however, happens in the `processScopeClose()`
1629
         * function using the data gathered by this function.
1630
         *
1631
         * Some variables are used in more complex ways, so there are other similar
1632
         * functions to this one, like `processVariableInString`, and
1633
         * `processCompact`. They have the same purpose as this function, though.
1634
         *
1635
         * If the 'ignore-for-loops' option is true, we will ignore the special
1636
         * processing for the increment variables of for loops. This will prevent
1637
         * infinite loops.
1638
         *
1639
         * @param File                           $phpcsFile The PHP_CodeSniffer file where this token was found.
1640
         * @param int                            $stackPtr  The position where the token was found.
1641
         * @param array<string, bool|string|int> $options   See above.
1642
         *
1643
         * @return void
1644
         */
1645
        protected function processVariable(File $phpcsFile, $stackPtr, $options = [])
356✔
1646
        {
1647
                $tokens = $phpcsFile->getTokens();
356✔
1648
                $token  = $tokens[$stackPtr];
356✔
1649

1650
                // Get the name of the variable.
1651
                $varName = Helpers::normalizeVarName($token['content']);
356✔
1652
                Helpers::debug("examining token for variable '{$varName}' on line {$token['line']}", $token);
356✔
1653

1654
                // Find the start of the current scope.
1655
                $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr);
356✔
1656
                if ($currScope === null) {
356✔
1657
                        Helpers::debug('no scope found');
52✔
1658
                        return;
52✔
1659
                }
1660
                Helpers::debug("start of scope for variable '{$varName}' is", $currScope);
352✔
1661

1662
                // Determine if variable is being assigned ("write") or used ("read").
1663

1664
                // Read methods that preempt assignment:
1665
                //   Are we a $object->$property type symbolic reference?
1666

1667
                // Possible assignment methods:
1668
                //   Is a mandatory function/closure parameter
1669
                //   Is an optional function/closure parameter with non-null value
1670
                //   Is closure use declaration of a variable defined within containing scope
1671
                //   catch (...) block start
1672
                //   $this within a class.
1673
                //   $GLOBALS, $_REQUEST, etc superglobals.
1674
                //   $var part of class::$var static member
1675
                //   Assignment via =
1676
                //   Assignment via list (...) =
1677
                //   Declares as a global
1678
                //   Declares as a static
1679
                //   Assignment via foreach (... as ...) { }
1680
                //   Pass-by-reference to known pass-by-reference function
1681

1682
                // Are we inside the third expression of a for loop? Store such variables
1683
                // for processing after the loop ends by `processClosingForLoopsAt()`.
1684
                if (empty($options['ignore-for-loops'])) {
352✔
1685
                        $forLoop = Helpers::getForLoopForIncrementVariable($stackPtr, $this->forLoops);
352✔
1686
                        if ($forLoop) {
352✔
1687
                                Helpers::debug('found variable inside for loop third expression');
8✔
1688
                                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
8✔
1689
                                $forLoop->incrementVariables[$stackPtr] = $varInfo;
8✔
1690
                                return;
8✔
1691
                        }
1692
                }
88✔
1693

1694
                // Are we a $object->$property type symbolic reference?
1695
                if ($this->processVariableAsSymbolicObjectProperty($phpcsFile, $stackPtr, $varName, $currScope)) {
352✔
1696
                        Helpers::debug('found symbolic object property');
28✔
1697
                        return;
28✔
1698
                }
1699

1700
                // Are we a function or closure parameter?
1701
                if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
352✔
1702
                        Helpers::debug('found function definition parameter');
232✔
1703
                        $this->processVariableAsFunctionParameter($phpcsFile, $stackPtr, $varName, $currScope);
232✔
1704
                        return;
232✔
1705
                }
1706

1707
                // Are we a variable being imported into a function's scope with "use"?
1708
                if (Helpers::isTokenInsideFunctionUseImport($phpcsFile, $stackPtr)) {
352✔
1709
                        Helpers::debug('found use scope import definition');
24✔
1710
                        $this->processVariableAsUseImportDefinition($phpcsFile, $stackPtr, $varName, $currScope);
24✔
1711
                        return;
24✔
1712
                }
1713

1714
                // Are we a catch parameter?
1715
                if ($this->processVariableAsCatchBlock($phpcsFile, $stackPtr, $varName, $currScope)) {
352✔
1716
                        Helpers::debug('found catch block');
44✔
1717
                        return;
44✔
1718
                }
1719

1720
                // Are we $this within a class?
1721
                if ($this->processVariableAsThisWithinClass($phpcsFile, $stackPtr, $varName)) {
352✔
1722
                        Helpers::debug('found this usage within a class');
72✔
1723
                        return;
72✔
1724
                }
1725

1726
                // Are we a $GLOBALS, $_REQUEST, etc superglobal?
1727
                if ($this->processVariableAsSuperGlobal($varName)) {
344✔
1728
                        Helpers::debug('found superglobal');
12✔
1729
                        return;
12✔
1730
                }
1731

1732
                // Check for static members used outside a class
1733
                if ($this->processVariableAsStaticOutsideClass($phpcsFile, $stackPtr, $varName)) {
344✔
1734
                        Helpers::debug('found static usage outside of class');
8✔
1735
                        return;
8✔
1736
                }
1737

1738
                // $var part of class::$var static member
1739
                if ($this->processVariableAsStaticMember($phpcsFile, $stackPtr)) {
344✔
1740
                        Helpers::debug('found static member');
28✔
1741
                        return;
28✔
1742
                }
1743

1744
                if ($this->processVariableAsClassProperty($phpcsFile, $stackPtr)) {
344✔
1745
                        Helpers::debug('found class property definition');
4✔
1746
                        return;
4✔
1747
                }
1748

1749
                // Is the next non-whitespace an assignment?
1750
                if (Helpers::isTokenInsideAssignmentLHS($phpcsFile, $stackPtr)) {
344✔
1751
                        Helpers::debug('found assignment');
324✔
1752
                        $this->processVariableAsAssignment($phpcsFile, $stackPtr, $varName, $currScope);
324✔
1753
                        if (Helpers::isTokenInsideAssignmentRHS($phpcsFile, $stackPtr) || Helpers::isTokenInsideFunctionCallArgument($phpcsFile, $stackPtr)) {
324✔
1754
                                Helpers::debug("found assignment that's also inside an expression");
12✔
1755
                                $this->markVariableRead($varName, $stackPtr, $currScope);
12✔
1756
                                return;
12✔
1757
                        }
1758
                        return;
324✔
1759
                }
1760

1761
                // OK, are we within a list (...) = construct?
1762
                if ($this->processVariableAsListAssignment($phpcsFile, $stackPtr, $varName, $currScope)) {
336✔
1763
                        Helpers::debug('found list assignment');
32✔
1764
                        return;
32✔
1765
                }
1766

1767
                // OK, are we within a [...] = construct?
1768
                if ($this->processVariableAsListShorthandAssignment($phpcsFile, $stackPtr, $varName, $currScope)) {
336✔
1769
                        Helpers::debug('found list shorthand assignment');
32✔
1770
                        return;
32✔
1771
                }
1772

1773
                // Are we a global declaration?
1774
                if ($this->processVariableAsGlobalDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) {
336✔
1775
                        Helpers::debug('found global declaration');
36✔
1776
                        return;
36✔
1777
                }
1778

1779
                // Are we a static declaration?
1780
                if ($this->processVariableAsStaticDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) {
336✔
1781
                        Helpers::debug('found static declaration');
40✔
1782
                        return;
40✔
1783
                }
1784

1785
                // Are we a foreach loopvar?
1786
                if ($this->processVariableAsForeachLoopVar($phpcsFile, $stackPtr, $varName, $currScope)) {
332✔
1787
                        Helpers::debug('found foreach loop variable');
64✔
1788
                        return;
64✔
1789
                }
1790

1791
                // Are we pass-by-reference to known pass-by-reference function?
1792
                if ($this->processVariableAsPassByReferenceFunctionCall($phpcsFile, $stackPtr, $varName, $currScope)) {
332✔
1793
                        Helpers::debug('found pass by reference');
20✔
1794
                        return;
20✔
1795
                }
1796

1797
                // Are we a numeric variable used for constructs like preg_replace?
1798
                if (Helpers::isVariableANumericVariable($varName)) {
332✔
1799
                        Helpers::debug('found numeric variable');
×
1800
                        return;
×
1801
                }
1802

1803
                if (Helpers::isVariableInsideElseCondition($phpcsFile, $stackPtr) || Helpers::isVariableInsideElseBody($phpcsFile, $stackPtr)) {
332✔
1804
                        Helpers::debug('found variable inside else condition or body');
24✔
1805
                        $this->processVaribleInsideElse($phpcsFile, $stackPtr, $varName, $currScope);
24✔
1806
                        return;
24✔
1807
                }
1808

1809
                // Are we an isset or empty call?
1810
                if (Helpers::isVariableInsideIssetOrEmpty($phpcsFile, $stackPtr)) {
332✔
1811
                        Helpers::debug('found isset or empty');
20✔
1812
                        $this->markVariableRead($varName, $stackPtr, $currScope);
20✔
1813
                        return;
20✔
1814
                }
1815

1816
                // OK, we don't appear to be a write to the var, assume we're a read.
1817
                Helpers::debug('looks like a variable read');
332✔
1818
                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
332✔
1819
        }
166✔
1820

1821
        /**
1822
         * @param File   $phpcsFile
1823
         * @param int    $stackPtr
1824
         * @param string $varName
1825
         * @param int    $currScope
1826
         *
1827
         * @return void
1828
         */
1829
        protected function processVaribleInsideElse(File $phpcsFile, $stackPtr, $varName, $currScope)
24✔
1830
        {
1831
                // Find all assignments to this variable inside the current scope.
1832
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
24✔
1833
                $allAssignmentIndices = array_unique($varInfo->allAssignments);
24✔
1834
                // Find the attached 'if' and 'elseif' block start and end indices.
1835
                $blockIndices = Helpers::getAttachedBlockIndicesForElse($phpcsFile, $stackPtr);
24✔
1836

1837
                // If all of the assignments are within the previous attached blocks, then warn about undefined.
1838
                $tokens = $phpcsFile->getTokens();
24✔
1839
                $assignmentsInsideAttachedBlocks = [];
24✔
1840
                foreach ($allAssignmentIndices as $index) {
24✔
1841
                        foreach ($blockIndices as $blockIndex) {
24✔
1842
                                $blockToken = $tokens[$blockIndex];
24✔
1843
                                Helpers::debug('for variable inside else, looking at assignment', $index, 'at block index', $blockIndex, 'which is token', $blockToken);
24✔
1844
                                if (isset($blockToken['scope_opener']) && isset($blockToken['scope_closer'])) {
24✔
1845
                                        $scopeOpener = $blockToken['scope_opener'];
16✔
1846
                                        $scopeCloser = $blockToken['scope_closer'];
16✔
1847
                                } else {
4✔
1848
                                        // If the `if` statement has no scope, it is probably inline, which
1849
                                        // means its scope is from the end of the condition up until the next
1850
                                        // semicolon
1851
                                        $scopeOpener = isset($blockToken['parenthesis_closer']) ? $blockToken['parenthesis_closer'] : $blockIndex + 1;
8✔
1852
                                        $scopeCloser = $phpcsFile->findNext([T_SEMICOLON], $scopeOpener);
8✔
1853
                                        if (! $scopeCloser) {
8✔
1854
                                                throw new \Exception("Cannot find scope for if condition block at index {$stackPtr} while examining variable {$varName}");
×
1855
                                        }
1856
                                }
1857
                                Helpers::debug('for variable inside else, looking at scope', $index, 'between', $scopeOpener, 'and', $scopeCloser);
24✔
1858
                                if (Helpers::isIndexInsideScope($index, $scopeOpener, $scopeCloser)) {
24✔
1859
                                        $assignmentsInsideAttachedBlocks[] = $index;
16✔
1860
                                }
4✔
1861
                        }
6✔
1862
                }
6✔
1863

1864
                if (count($assignmentsInsideAttachedBlocks) === count($allAssignmentIndices)) {
24✔
1865
                        if (! $varInfo->ignoreUndefined) {
16✔
1866
                                Helpers::debug("variable $varName inside else looks undefined");
16✔
1867
                                $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
16✔
1868
                        }
4✔
1869
                        return;
16✔
1870
                }
1871

1872
                Helpers::debug('looks like a variable read inside else');
24✔
1873
                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
24✔
1874
        }
12✔
1875

1876
        /**
1877
         * Called to process variables found in double quoted strings.
1878
         *
1879
         * Note that there may be more than one variable in the string, which will
1880
         * result only in one call for the string.
1881
         *
1882
         * @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
1883
         * @param int  $stackPtr  The position where the double quoted string was found.
1884
         *
1885
         * @return void
1886
         */
1887
        protected function processVariableInString(File $phpcsFile, $stackPtr)
168✔
1888
        {
1889
                $tokens = $phpcsFile->getTokens();
168✔
1890
                $token  = $tokens[$stackPtr];
168✔
1891

1892
                $regexp = Constants::getDoubleQuotedVarRegexp();
168✔
1893
                if (! empty($regexp) && !preg_match_all($regexp, $token['content'], $matches)) {
168✔
1894
                        Helpers::debug('processVariableInString: no variables found', $token);
28✔
1895
                        return;
28✔
1896
                }
1897
                Helpers::debug('examining token for variable in string', $token);
148✔
1898

1899
                if (empty($matches)) {
148✔
1900
                        Helpers::debug('processVariableInString: no variables found after search', $token);
×
1901
                        return;
×
1902
                }
1903
                foreach ($matches[1] as $varName) {
148✔
1904
                        $varName = Helpers::normalizeVarName($varName);
148✔
1905

1906
                        // Are we $this within a class?
1907
                        if ($this->processVariableAsThisWithinClass($phpcsFile, $stackPtr, $varName)) {
148✔
1908
                                continue;
8✔
1909
                        }
1910

1911
                        if ($this->processVariableAsSuperGlobal($varName)) {
140✔
1912
                                continue;
12✔
1913
                        }
1914

1915
                        // Are we a numeric variable used for constructs like preg_replace?
1916
                        if (Helpers::isVariableANumericVariable($varName)) {
140✔
1917
                                continue;
4✔
1918
                        }
1919

1920
                        $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr, $varName);
140✔
1921
                        if ($currScope === null) {
140✔
1922
                                continue;
×
1923
                        }
1924

1925
                        $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
140✔
1926
                }
37✔
1927
        }
74✔
1928

1929
        /**
1930
         * Called to process variables named in a call to compact().
1931
         *
1932
         * @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
1933
         * @param int  $stackPtr  The position where the call to compact() was found.
1934
         *
1935
         * @return void
1936
         */
1937
        protected function processCompact(File $phpcsFile, $stackPtr)
12✔
1938
        {
1939
                Helpers::debug("processCompact at {$stackPtr}");
12✔
1940
                $arguments = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
12✔
1941
                $variables = Helpers::getVariablesInsideCompact($phpcsFile, $stackPtr, $arguments);
12✔
1942
                foreach ($variables as $variable) {
12✔
1943
                        $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr, $variable->name);
12✔
1944
                        if ($currScope === null) {
12✔
1945
                                continue;
×
1946
                        }
1947
                        $variablePosition = $variable->firstRead ? $variable->firstRead : $stackPtr;
12✔
1948
                        $this->markVariableReadAndWarnIfUndefined($phpcsFile, $variable->name, $variablePosition, $currScope);
12✔
1949
                }
3✔
1950
        }
6✔
1951

1952
        /**
1953
         * Called to process the end of a scope.
1954
         *
1955
         * Note that although triggered by the closing curly brace of the scope,
1956
         * $stackPtr is the scope conditional, not the closing curly brace.
1957
         *
1958
         * @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
1959
         * @param int  $stackPtr  The position of the scope conditional.
1960
         *
1961
         * @return void
1962
         */
1963
        protected function processScopeClose(File $phpcsFile, $stackPtr)
356✔
1964
        {
1965
                Helpers::debug("processScopeClose at {$stackPtr}");
356✔
1966
                $scopeInfo = $this->scopeManager->getScopeForScopeStart($phpcsFile->getFilename(), $stackPtr);
356✔
1967
                if (is_null($scopeInfo)) {
356✔
1968
                        return;
×
1969
                }
1970
                foreach ($scopeInfo->variables as $varInfo) {
356✔
1971
                        $this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
344✔
1972
                }
89✔
1973
        }
178✔
1974

1975
        /**
1976
         * Warn about an unused variable if it has not been used within a scope.
1977
         *
1978
         * @param File         $phpcsFile
1979
         * @param VariableInfo $varInfo
1980
         * @param ScopeInfo    $scopeInfo
1981
         *
1982
         * @return void
1983
         */
1984
        protected function processScopeCloseForVariable(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
344✔
1985
        {
1986
                Helpers::debug('processScopeCloseForVariable', $varInfo);
344✔
1987
                if ($varInfo->ignoreUnused || isset($varInfo->firstRead)) {
344✔
1988
                        return;
340✔
1989
                }
1990
                if ($this->allowUnusedFunctionParameters && $varInfo->scopeType === ScopeType::PARAM) {
244✔
1991
                        return;
4✔
1992
                }
1993
                if ($this->allowUnusedParametersBeforeUsed && $varInfo->scopeType === ScopeType::PARAM && Helpers::areFollowingArgumentsUsed($varInfo, $scopeInfo)) {
240✔
1994
                        Helpers::debug("variable '{$varInfo->name}' at end of scope has unused following args");
16✔
1995
                        return;
16✔
1996
                }
1997
                if ($this->allowUnusedForeachVariables && $varInfo->isForeachLoopAssociativeValue) {
240✔
1998
                        return;
12✔
1999
                }
2000
                if ($varInfo->referencedVariableScope !== null && isset($varInfo->firstInitialized)) {
240✔
2001
                        Helpers::debug("variable '{$varInfo->name}' at end of scope is a reference and so counts as used");
36✔
2002
                        // If we're pass-by-reference then it's a common pattern to
2003
                        // use the variable to return data to the caller, so any
2004
                        // assignment also counts as "variable use" for the purposes
2005
                        // of "unused variable" warnings.
2006
                        return;
36✔
2007
                }
2008
                if ($varInfo->scopeType === ScopeType::GLOBALSCOPE && isset($varInfo->firstInitialized)) {
236✔
2009
                        Helpers::debug("variable '{$varInfo->name}' at end of scope is a global and so counts as used");
12✔
2010
                        // If we imported this variable from the global scope, any further use of
2011
                        // the variable, including assignment, should count as "variable use" for
2012
                        // the purposes of "unused variable" warnings.
2013
                        return;
12✔
2014
                }
2015
                if (empty($varInfo->firstDeclared) && empty($varInfo->firstInitialized)) {
236✔
2016
                        return;
16✔
2017
                }
2018
                if ($this->allowUnusedVariablesBeforeRequire && Helpers::isRequireInScopeAfter($phpcsFile, $varInfo, $scopeInfo)) {
236✔
2019
                        return;
4✔
2020
                }
2021
                if ($scopeInfo->scopeStartIndex === 0 && $this->allowUnusedVariablesInFileScope) {
232✔
2022
                        return;
4✔
2023
                }
2024
                if (
2025
                        ! empty($varInfo->firstDeclared)
228✔
2026
                        && $varInfo->scopeType === ScopeType::PARAM
228✔
2027
                        && Helpers::isInAbstractClass(
198✔
2028
                                $phpcsFile,
168✔
2029
                                Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $varInfo->firstDeclared) ?: 0
198✔
2030
                        )
141✔
2031
                        && Helpers::isFunctionBodyEmpty(
173✔
2032
                                $phpcsFile,
118✔
2033
                                Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $varInfo->firstDeclared) ?: 0
173✔
2034
                        )
116✔
2035
                ) {
57✔
2036
                        // Allow non-abstract methods inside an abstract class to have unused
2037
                        // parameters if the method body does nothing. Such methods are
2038
                        // effectively optional abstract methods so their unused parameters
2039
                        // should be ignored as we do with abstract method parameters.
2040
                        return;
8✔
2041
                }
2042

2043
                $this->warnAboutUnusedVariable($phpcsFile, $varInfo);
228✔
2044
        }
114✔
2045

2046
        /**
2047
         * Register warnings for a variable that is defined but not used.
2048
         *
2049
         * @param File         $phpcsFile
2050
         * @param VariableInfo $varInfo
2051
         *
2052
         * @return void
2053
         */
2054
        protected function warnAboutUnusedVariable(File $phpcsFile, VariableInfo $varInfo)
228✔
2055
        {
2056
                foreach (array_unique($varInfo->allAssignments) as $indexForWarning) {
228✔
2057
                        Helpers::debug("variable '{$varInfo->name}' at end of scope looks unused");
228✔
2058
                        $phpcsFile->addWarning(
228✔
2059
                                'Unused %s %s.',
228✔
2060
                                $indexForWarning,
228✔
2061
                                'UnusedVariable',
228✔
2062
                                [
114✔
2063
                                        VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType ?: ScopeType::LOCAL],
228✔
2064
                                        "\${$varInfo->name}",
228✔
2065
                                ]
114✔
2066
                        );
171✔
2067
                }
57✔
2068
        }
114✔
2069

2070
        /**
2071
         * @param File   $phpcsFile
2072
         * @param string $varName
2073
         * @param int    $stackPtr
2074
         *
2075
         * @return void
2076
         */
2077
        protected function warnAboutUndefinedVariable(File $phpcsFile, $varName, $stackPtr)
252✔
2078
        {
2079
                $phpcsFile->addWarning(
252✔
2080
                        'Variable %s is undefined.',
252✔
2081
                        $stackPtr,
252✔
2082
                        'UndefinedVariable',
252✔
2083
                        ["\${$varName}"]
252✔
2084
                );
189✔
2085
        }
126✔
2086

2087
        /**
2088
         * @param File   $phpcsFile
2089
         * @param string $varName
2090
         * @param int    $stackPtr
2091
         *
2092
         * @return void
2093
         */
2094
        protected function warnAboutUndefinedArrayPushShortcut(File $phpcsFile, $varName, $stackPtr)
28✔
2095
        {
2096
                $phpcsFile->addWarning(
28✔
2097
                        'Array variable %s is undefined.',
28✔
2098
                        $stackPtr,
28✔
2099
                        'UndefinedVariable',
28✔
2100
                        ["\${$varName}"]
28✔
2101
                );
21✔
2102
        }
14✔
2103

2104
        /**
2105
         * @param File   $phpcsFile
2106
         * @param string $varName
2107
         * @param int    $stackPtr
2108
         *
2109
         * @return void
2110
         */
2111
        protected function warnAboutUndefinedUnset(File $phpcsFile, $varName, $stackPtr)
8✔
2112
        {
2113
                $phpcsFile->addWarning(
8✔
2114
                        'Variable %s inside unset call is undefined.',
8✔
2115
                        $stackPtr,
8✔
2116
                        'UndefinedUnsetVariable',
8✔
2117
                        ["\${$varName}"]
8✔
2118
                );
6✔
2119
        }
4✔
2120
}
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