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

sirbrillig / phpcs-variable-analysis / 13853172128

14 Mar 2025 08:59AM UTC coverage: 93.812%. Remained the same
13853172128

Pull #350

github

jrfnl
GH Actions: use the xmllint-validate action runner

Instead of doing all the installation steps for xmllint validation in the workflow, use the :sparkles: new dedicated `phpcsstandards/xmllint-validate` action runner instead.

Ref: https://github.com/marketplace/actions/xmllint-validate
Pull Request #350: GH Actions: use the xmllint-validate action runner

1880 of 2004 relevant lines covered (93.81%)

137.02 hits per line

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

96.46
/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
        public function __construct()
352✔
157
        {
158
                $this->scopeManager = new ScopeManager();
352✔
159
        }
176✔
160

161
        /**
162
         * Decide which tokens to scan.
163
         *
164
         * @return (int|string)[]
165
         */
166
        public function register()
352✔
167
        {
168
                $types = [
176✔
169
                        T_VARIABLE,
352✔
170
                        T_DOUBLE_QUOTED_STRING,
352✔
171
                        T_HEREDOC,
352✔
172
                        T_CLOSE_CURLY_BRACKET,
352✔
173
                        T_FUNCTION,
352✔
174
                        T_CLOSURE,
352✔
175
                        T_STRING,
352✔
176
                        T_COMMA,
352✔
177
                        T_SEMICOLON,
352✔
178
                        T_CLOSE_PARENTHESIS,
352✔
179
                        T_FOR,
352✔
180
                        T_ENDFOR,
352✔
181
                ];
352✔
182
                if (defined('T_FN')) {
352✔
183
                        $types[] = T_FN;
352✔
184
                }
176✔
185
                if (defined('T_ENUM')) {
352✔
186
                        $types[] = T_ENUM;
176✔
187
                }
88✔
188
                return $types;
352✔
189
        }
190

191
        /**
192
         * @param string $functionName
193
         *
194
         * @return array<int|string>
195
         */
196
        private function getPassByReferenceFunction($functionName)
158✔
197
        {
198
                $passByRefFunctions = Constants::getPassByReferenceFunctions();
158✔
199
                if (!empty($this->sitePassByRefFunctions)) {
158✔
200
                        $lines = Helpers::splitStringToArray('/\s+/', trim($this->sitePassByRefFunctions));
4✔
201
                        foreach ($lines as $line) {
4✔
202
                                list ($function, $args) = explode(':', $line);
4✔
203
                                $passByRefFunctions[$function] = explode(',', $args);
4✔
204
                        }
2✔
205
                }
2✔
206
                if ($this->allowWordPressPassByRefFunctions) {
158✔
207
                        $passByRefFunctions = array_merge($passByRefFunctions, Constants::getWordPressPassByReferenceFunctions());
4✔
208
                }
2✔
209
                return isset($passByRefFunctions[$functionName]) ? $passByRefFunctions[$functionName] : [];
158✔
210
        }
211

212
        /**
213
         * Scan and process a token.
214
         *
215
         * This is the main processing function of the sniff. Will run on every token
216
         * for which `register()` returns true.
217
         *
218
         * @param File $phpcsFile
219
         * @param int  $stackPtr
220
         *
221
         * @return void
222
         */
223
        public function process(File $phpcsFile, $stackPtr)
352✔
224
        {
225
                $tokens = $phpcsFile->getTokens();
352✔
226

227
                $scopeStartTokenTypes = [
176✔
228
                        T_FUNCTION,
352✔
229
                        T_CLOSURE,
352✔
230
                ];
352✔
231

232
                $token = $tokens[$stackPtr];
352✔
233

234
                // Cache the current PHPCS File in an instance variable so it can be more
235
                // easily accessed in other places which aren't passed the object.
236
                if ($this->currentFile !== $phpcsFile) {
352✔
237
                        $this->currentFile = $phpcsFile;
352✔
238
                        $this->forLoops = [];
352✔
239
                        $this->enums = [];
352✔
240
                }
176✔
241

242
                // Add the global scope for the current file to our scope indexes.
243
                $scopesForFilename = $this->scopeManager->getScopesForFilename($phpcsFile->getFilename());
352✔
244
                if (empty($scopesForFilename)) {
352✔
245
                        $this->scopeManager->recordScopeStartAndEnd($phpcsFile, 0);
352✔
246
                }
176✔
247

248
                // Find and process variables to perform two jobs: to record variable
249
                // definition or use, and to report variables as undefined if they are used
250
                // without having been first defined.
251
                if ($token['code'] === T_VARIABLE) {
352✔
252
                        $this->processVariable($phpcsFile, $stackPtr);
352✔
253
                }
176✔
254

255
                // Report variables defined but not used in the current scope as unused
256
                // variables if the current token closes scopes.
257
                $this->searchForAndProcessClosingScopesAt($phpcsFile, $stackPtr);
352✔
258

259
                // Scan variables that were postponed because they exist in the increment
260
                // expression of a for loop if the current token closes a loop.
261
                $this->processClosingForLoopsAt($phpcsFile, $stackPtr);
352✔
262

263
                if ($token['code'] === T_VARIABLE) {
352✔
264
                        return;
352✔
265
                }
266

267
                if (($token['code'] === T_DOUBLE_QUOTED_STRING) || ($token['code'] === T_HEREDOC)) {
352✔
268
                        $this->processVariableInString($phpcsFile, $stackPtr);
164✔
269
                        return;
164✔
270
                }
271
                if (($token['code'] === T_STRING) && ($token['content'] === 'compact')) {
352✔
272
                        $this->processCompact($phpcsFile, $stackPtr);
12✔
273
                        return;
12✔
274
                }
275

276
                // Record for loop boundaries so we can delay scanning the third for loop
277
                // expression until after the loop has been scanned.
278
                if ($token['code'] === T_FOR) {
352✔
279
                        $this->recordForLoop($phpcsFile, $stackPtr);
8✔
280
                        return;
8✔
281
                }
282

283
                // Record enums so we can detect them even before phpcs was able to.
284
                if ($token['content'] === 'enum') {
352✔
285
                        $enumInfo = Helpers::makeEnumInfo($phpcsFile, $stackPtr);
4✔
286
                        // The token might not actually be an enum so let's avoid returning if
287
                        // it's not.
288
                        if ($enumInfo) {
4✔
289
                                $this->enums[$stackPtr] = $enumInfo;
4✔
290
                                return;
4✔
291
                        }
292
                }
2✔
293

294
                // If the current token is a call to `get_defined_vars()`, consider that a
295
                // usage of all variables in the current scope.
296
                if ($this->isGetDefinedVars($phpcsFile, $stackPtr)) {
352✔
297
                        Helpers::debug('get_defined_vars is being called');
4✔
298
                        $this->markAllVariablesRead($phpcsFile, $stackPtr);
4✔
299
                        return;
4✔
300
                }
301

302
                // If the current token starts a scope, record that scope's start and end
303
                // indexes so that we can determine if variables in that scope are defined
304
                // and/or used.
305
                if (
306
                        in_array($token['code'], $scopeStartTokenTypes, true) ||
352✔
307
                        Helpers::isArrowFunction($phpcsFile, $stackPtr)
352✔
308
                ) {
176✔
309
                        Helpers::debug('found scope condition', $token);
336✔
310
                        $this->scopeManager->recordScopeStartAndEnd($phpcsFile, $stackPtr);
336✔
311
                        return;
336✔
312
                }
313
        }
176✔
314

315
        /**
316
         * Record the boundaries of a for loop.
317
         *
318
         * @param File $phpcsFile
319
         * @param int  $stackPtr
320
         *
321
         * @return void
322
         */
323
        private function recordForLoop($phpcsFile, $stackPtr)
8✔
324
        {
325
                $this->forLoops[$stackPtr] = Helpers::makeForLoopInfo($phpcsFile, $stackPtr);
8✔
326
        }
4✔
327

328
        /**
329
         * Find scopes closed by a token and process their variables.
330
         *
331
         * Calls `processScopeClose()` for each closed scope.
332
         *
333
         * @param File $phpcsFile
334
         * @param int  $stackPtr
335
         *
336
         * @return void
337
         */
338
        private function searchForAndProcessClosingScopesAt($phpcsFile, $stackPtr)
352✔
339
        {
340
                $scopeIndicesThisCloses = $this->scopeManager->getScopesForScopeEnd($phpcsFile->getFilename(), $stackPtr);
352✔
341

342
                $tokens = $phpcsFile->getTokens();
352✔
343
                $token = $tokens[$stackPtr];
352✔
344
                $line = $token['line'];
352✔
345
                foreach ($scopeIndicesThisCloses as $scopeIndexThisCloses) {
352✔
346
                        Helpers::debug('found closing scope at index', $stackPtr, 'line', $line, 'for scopes starting at:', $scopeIndexThisCloses->scopeStartIndex);
352✔
347
                        $this->processScopeClose($phpcsFile, $scopeIndexThisCloses->scopeStartIndex);
352✔
348
                }
176✔
349
        }
176✔
350

351
        /**
352
         * Scan variables that were postponed because they exist in the increment expression of a for loop.
353
         *
354
         * @param File $phpcsFile
355
         * @param int  $stackPtr
356
         *
357
         * @return void
358
         */
359
        private function processClosingForLoopsAt($phpcsFile, $stackPtr)
352✔
360
        {
361
                $forLoopsThisCloses = [];
352✔
362
                foreach ($this->forLoops as $forLoop) {
352✔
363
                        if ($forLoop->blockEnd === $stackPtr) {
8✔
364
                                $forLoopsThisCloses[] = $forLoop;
8✔
365
                        }
4✔
366
                }
176✔
367

368
                foreach ($forLoopsThisCloses as $forLoop) {
352✔
369
                        foreach ($forLoop->incrementVariables as $varIndex => $varInfo) {
8✔
370
                                Helpers::debug('processing delayed for loop increment variable at', $varIndex, $varInfo);
8✔
371
                                $this->processVariable($phpcsFile, $varIndex, ['ignore-for-loops' => true]);
8✔
372
                        }
4✔
373
                }
176✔
374
        }
176✔
375

376
        /**
377
         * Return true if the token is a call to `get_defined_vars()`.
378
         *
379
         * @param File $phpcsFile
380
         * @param int  $stackPtr
381
         *
382
         * @return bool
383
         */
384
        protected function isGetDefinedVars(File $phpcsFile, $stackPtr)
352✔
385
        {
386
                $tokens = $phpcsFile->getTokens();
352✔
387
                $token = $tokens[$stackPtr];
352✔
388
                if (! $token || $token['content'] !== 'get_defined_vars') {
352✔
389
                        return false;
352✔
390
                }
391
                // Make sure this is a function call
392
                $parenPointer = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
4✔
393
                if (! $parenPointer || $tokens[$parenPointer]['code'] !== T_OPEN_PARENTHESIS) {
4✔
394
                        return false;
×
395
                }
396
                return true;
4✔
397
        }
398

399
        /**
400
         * @return string
401
         */
402
        protected function getFilename()
340✔
403
        {
404
                return $this->currentFile ? $this->currentFile->getFilename() : 'unknown file';
340✔
405
        }
406

407
        /**
408
         * @param int $currScope
409
         *
410
         * @return ScopeInfo
411
         */
412
        protected function getOrCreateScopeInfo($currScope)
340✔
413
        {
414
                $scope = $this->scopeManager->getScopeForScopeStart($this->getFilename(), $currScope);
340✔
415
                if (! $scope) {
340✔
416
                        if (! $this->currentFile) {
×
417
                                throw new \Exception('Cannot create scope info; current file is not set.');
×
418
                        }
419
                        $scope = $this->scopeManager->recordScopeStartAndEnd($this->currentFile, $currScope);
×
420
                }
421
                return $scope;
340✔
422
        }
423

424
        /**
425
         * @param string $varName
426
         * @param int    $currScope
427
         *
428
         * @return VariableInfo|null
429
         */
430
        protected function getVariableInfo($varName, $currScope)
20✔
431
        {
432
                $scopeInfo = $this->scopeManager->getScopeForScopeStart($this->getFilename(), $currScope);
20✔
433
                return ($scopeInfo && isset($scopeInfo->variables[$varName])) ? $scopeInfo->variables[$varName] : null;
20✔
434
        }
435

436
        /**
437
         * Returns variable data for a variable at an index.
438
         *
439
         * The variable will also be added to the list of variables stored in its
440
         * scope so that its use or non-use can be reported when those scopes end by
441
         * `processScopeClose()`.
442
         *
443
         * @param string $varName
444
         * @param int    $currScope
445
         *
446
         * @return VariableInfo
447
         */
448
        protected function getOrCreateVariableInfo($varName, $currScope)
340✔
449
        {
450
                Helpers::debug("getOrCreateVariableInfo: starting for '{$varName}'");
340✔
451
                $scopeInfo = $this->getOrCreateScopeInfo($currScope);
340✔
452
                if (isset($scopeInfo->variables[$varName])) {
340✔
453
                        Helpers::debug("getOrCreateVariableInfo: found variable for '{$varName}'", $scopeInfo->variables[$varName]);
340✔
454
                        return $scopeInfo->variables[$varName];
340✔
455
                }
456
                Helpers::debug("getOrCreateVariableInfo: creating a new variable for '{$varName}' in scope", $scopeInfo);
340✔
457
                $scopeInfo->variables[$varName] = new VariableInfo($varName);
340✔
458
                $validUnusedVariableNames = (empty($this->validUnusedVariableNames))
340✔
459
                ? []
338✔
460
                : Helpers::splitStringToArray('/\s+/', trim($this->validUnusedVariableNames));
256✔
461
                $validUndefinedVariableNames = (empty($this->validUndefinedVariableNames))
340✔
462
                ? []
332✔
463
                : Helpers::splitStringToArray('/\s+/', trim($this->validUndefinedVariableNames));
259✔
464
                if (in_array($varName, $validUnusedVariableNames)) {
340✔
465
                        $scopeInfo->variables[$varName]->ignoreUnused = true;
4✔
466
                }
2✔
467
                if (! empty($this->ignoreUnusedRegexp) && preg_match($this->ignoreUnusedRegexp, $varName) === 1) {
340✔
468
                        $scopeInfo->variables[$varName]->ignoreUnused = true;
8✔
469
                }
4✔
470
                if ($scopeInfo->scopeStartIndex === 0 && $this->allowUndefinedVariablesInFileScope) {
340✔
471
                        $scopeInfo->variables[$varName]->ignoreUndefined = true;
4✔
472
                }
2✔
473
                if (in_array($varName, $validUndefinedVariableNames)) {
340✔
474
                        $scopeInfo->variables[$varName]->ignoreUndefined = true;
16✔
475
                }
8✔
476
                if (! empty($this->validUndefinedVariableRegexp) && preg_match($this->validUndefinedVariableRegexp, $varName) === 1) {
340✔
477
                        $scopeInfo->variables[$varName]->ignoreUndefined = true;
4✔
478
                }
2✔
479
                Helpers::debug("getOrCreateVariableInfo: scope for '{$varName}' is now", $scopeInfo);
340✔
480
                return $scopeInfo->variables[$varName];
340✔
481
        }
482

483
        /**
484
         * Record that a variable has been defined and assigned a value.
485
         *
486
         * If a variable has been defined within a scope, it will not be marked as
487
         * undefined when that variable is later used. If it is not used, it will be
488
         * marked as unused when that scope ends.
489
         *
490
         * Sometimes it's possible to assign something to a variable without
491
         * definining it (eg: assignment to a reference); in that case, use
492
         * `markVariableAssignmentWithoutInitialization()`.
493
         *
494
         * @param string $varName
495
         * @param int    $stackPtr
496
         * @param int    $currScope
497
         *
498
         * @return void
499
         */
500
        protected function markVariableAssignment($varName, $stackPtr, $currScope)
324✔
501
        {
502
                Helpers::debug('markVariableAssignment: starting for', $varName);
324✔
503
                $this->markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope);
324✔
504
                Helpers::debug('markVariableAssignment: marked as assigned without initialization', $varName);
324✔
505
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
324✔
506
                if (isset($varInfo->firstInitialized) && ($varInfo->firstInitialized <= $stackPtr)) {
324✔
507
                        Helpers::debug('markVariableAssignment: variable is already initialized', $varName);
132✔
508
                        return;
132✔
509
                }
510
                $varInfo->firstInitialized = $stackPtr;
324✔
511
                Helpers::debug('markVariableAssignment: marked as initialized', $varName);
324✔
512
        }
162✔
513

514
        /**
515
         * Record that a variable has been assigned a value.
516
         *
517
         * Does not record that a variable has been defined, which is the usual state
518
         * of affairs. For that, use `markVariableAssignment()`.
519
         *
520
         * This is useful for assignments to references.
521
         *
522
         * @param string $varName
523
         * @param int    $stackPtr
524
         * @param int    $currScope
525
         *
526
         * @return void
527
         */
528
        protected function markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope)
324✔
529
        {
530
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
324✔
531

532
                // Is the variable referencing another variable? If so, mark that variable used also.
533
                if ($varInfo->referencedVariableScope !== null && $varInfo->referencedVariableScope !== $currScope) {
324✔
534
                        Helpers::debug('markVariableAssignmentWithoutInitialization: considering marking referenced variable assigned', $varName);
20✔
535
                        // 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
536
                        if ($this->getVariableInfo($varInfo->name, $varInfo->referencedVariableScope)) {
20✔
537
                                Helpers::debug('markVariableAssignmentWithoutInitialization: marking referenced variable as assigned also', $varName);
4✔
538
                                $this->markVariableAssignment($varInfo->name, $stackPtr, $varInfo->referencedVariableScope);
4✔
539
                        } else {
2✔
540
                                Helpers::debug('markVariableAssignmentWithoutInitialization: not marking referenced variable assigned', $varName);
17✔
541
                        }
542
                } else {
10✔
543
                                Helpers::debug('markVariableAssignmentWithoutInitialization: not considering referenced variable', $varName);
324✔
544
                }
545

546
                if (empty($varInfo->scopeType)) {
324✔
547
                        $varInfo->scopeType = ScopeType::LOCAL;
316✔
548
                }
158✔
549
                $varInfo->allAssignments[] = $stackPtr;
324✔
550
        }
162✔
551

552
        /**
553
         * Record that a variable has been defined within a scope.
554
         *
555
         * @param string                                                                                           $varName
556
         * @param ScopeType::PARAM|ScopeType::BOUND|ScopeType::LOCAL|ScopeType::GLOBALSCOPE|ScopeType::STATICSCOPE $scopeType
557
         * @param ?string                                                                                          $typeHint
558
         * @param int                                                                                              $stackPtr
559
         * @param int                                                                                              $currScope
560
         * @param ?bool                                                                                            $permitMatchingRedeclaration
561
         *
562
         * @return void
563
         */
564
        protected function markVariableDeclaration(
256✔
565
                $varName,
566
                $scopeType,
567
                $typeHint,
568
                $stackPtr,
569
                $currScope,
570
                $permitMatchingRedeclaration = false
571
        ) {
572
                Helpers::debug("marking variable '{$varName}' declared in scope starting at token", $currScope);
256✔
573
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
256✔
574

575
                if (! empty($varInfo->scopeType)) {
256✔
576
                        if (($permitMatchingRedeclaration === false) || ($varInfo->scopeType !== $scopeType)) {
16✔
577
                                //  Issue redeclaration/reuse warning
578
                                //  Note: we check off scopeType not firstDeclared, this is so that
579
                                //    we catch declarations that come after implicit declarations like
580
                                //    use of a variable as a local.
581
                                $this->addWarning(
8✔
582
                                        'Redeclaration of %s %s as %s.',
8✔
583
                                        $stackPtr,
8✔
584
                                        'VariableRedeclaration',
8✔
585
                                        [
4✔
586
                                                VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType],
8✔
587
                                                "\${$varName}",
8✔
588
                                                VariableInfo::$scopeTypeDescriptions[$scopeType],
8✔
589
                                        ]
4✔
590
                                );
8✔
591
                        }
4✔
592
                }
8✔
593

594
                $varInfo->scopeType = $scopeType;
256✔
595
                if (isset($typeHint)) {
256✔
596
                        $varInfo->typeHint = $typeHint;
×
597
                }
598
                if (isset($varInfo->firstDeclared) && ($varInfo->firstDeclared <= $stackPtr)) {
256✔
599
                        Helpers::debug("variable '{$varName}' was already marked declared", $varInfo);
16✔
600
                        return;
16✔
601
                }
602
                $varInfo->firstDeclared = $stackPtr;
256✔
603
                $varInfo->allAssignments[] = $stackPtr;
256✔
604
                Helpers::debug("variable '{$varName}' marked declared", $varInfo);
256✔
605
        }
128✔
606

607
        /**
608
         * @param string   $message
609
         * @param int      $stackPtr
610
         * @param string   $code
611
         * @param string[] $data
612
         *
613
         * @return void
614
         */
615
        protected function addWarning($message, $stackPtr, $code, $data)
8✔
616
        {
617
                if (! $this->currentFile) {
8✔
618
                        throw new \Exception('Cannot add warning; current file is not set.');
×
619
                }
620
                $this->currentFile->addWarning(
8✔
621
                        $message,
8✔
622
                        $stackPtr,
8✔
623
                        $code,
8✔
624
                        $data
4✔
625
                );
8✔
626
        }
4✔
627

628
        /**
629
         * Record that a variable has been used within a scope.
630
         *
631
         * If the variable has not been defined first, this will still mark it used.
632
         * To display a warning for undefined variables, use
633
         * `markVariableReadAndWarnIfUndefined()`.
634
         *
635
         * @param string $varName
636
         * @param int    $stackPtr
637
         * @param int    $currScope
638
         *
639
         * @return void
640
         */
641
        protected function markVariableRead($varName, $stackPtr, $currScope)
336✔
642
        {
643
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
336✔
644
                if (isset($varInfo->firstRead) && ($varInfo->firstRead <= $stackPtr)) {
336✔
645
                        return;
212✔
646
                }
647
                $varInfo->firstRead = $stackPtr;
336✔
648
        }
168✔
649

650
        /**
651
         * Return true if a variable is defined within a scope.
652
         *
653
         * @param string $varName
654
         * @param int    $stackPtr
655
         * @param int    $currScope
656
         *
657
         * @return bool
658
         */
659
        protected function isVariableUndefined($varName, $stackPtr, $currScope)
336✔
660
        {
661
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
336✔
662
                Helpers::debug('isVariableUndefined', $varInfo, 'at', $stackPtr);
336✔
663
                if ($varInfo->ignoreUndefined) {
336✔
664
                        return false;
24✔
665
                }
666
                if (isset($varInfo->firstDeclared) && $varInfo->firstDeclared <= $stackPtr) {
336✔
667
                        return false;
228✔
668
                }
669
                if (isset($varInfo->firstInitialized) && $varInfo->firstInitialized <= $stackPtr) {
328✔
670
                        return false;
296✔
671
                }
672
                // If we are inside a for loop increment expression, check to see if the
673
                // variable was defined inside the for loop.
674
                foreach ($this->forLoops as $forLoop) {
256✔
675
                        if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
8✔
676
                                Helpers::debug('isVariableUndefined looking at increment expression for loop', $forLoop);
8✔
677
                                if (
678
                                        isset($varInfo->firstInitialized)
8✔
679
                                        && $varInfo->firstInitialized > $forLoop->blockStart
8✔
680
                                        && $varInfo->firstInitialized < $forLoop->blockEnd
8✔
681
                                ) {
4✔
682
                                        return false;
8✔
683
                                }
684
                        }
4✔
685
                }
128✔
686
                // If we are inside a for loop body, check to see if the variable was
687
                // defined in that loop's third expression.
688
                foreach ($this->forLoops as $forLoop) {
256✔
689
                        if ($stackPtr > $forLoop->blockStart && $stackPtr < $forLoop->blockEnd) {
8✔
690
                                foreach ($forLoop->incrementVariables as $forLoopVarInfo) {
8✔
691
                                        if ($varInfo === $forLoopVarInfo) {
8✔
692
                                                return false;
×
693
                                        }
694
                                }
4✔
695
                        }
4✔
696
                }
128✔
697
                return true;
256✔
698
        }
699

700
        /**
701
         * Record a variable use and report a warning if the variable is undefined.
702
         *
703
         * @param File   $phpcsFile
704
         * @param string $varName
705
         * @param int    $stackPtr
706
         * @param int    $currScope
707
         *
708
         * @return void
709
         */
710
        protected function markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope)
336✔
711
        {
712
                $this->markVariableRead($varName, $stackPtr, $currScope);
336✔
713
                if ($this->isVariableUndefined($varName, $stackPtr, $currScope) === true) {
336✔
714
                        Helpers::debug("variable $varName looks undefined");
256✔
715

716
                        if (Helpers::isVariableArrayPushShortcut($phpcsFile, $stackPtr)) {
256✔
717
                                $this->warnAboutUndefinedArrayPushShortcut($phpcsFile, $varName, $stackPtr);
28✔
718
                                // Mark the variable as defined if it's of the form `$x[] = 1;`
719
                                $this->markVariableAssignment($varName, $stackPtr, $currScope);
28✔
720
                                return;
28✔
721
                        }
722

723
                        if (Helpers::isVariableInsideUnset($phpcsFile, $stackPtr)) {
256✔
724
                                $this->warnAboutUndefinedUnset($phpcsFile, $varName, $stackPtr);
8✔
725
                                return;
8✔
726
                        }
727

728
                        $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
248✔
729
                }
124✔
730
        }
168✔
731

732
        /**
733
         * Mark all variables within a scope as being used.
734
         *
735
         * This will prevent any of the variables in that scope from being reported
736
         * as unused.
737
         *
738
         * @param File $phpcsFile
739
         * @param int  $stackPtr
740
         *
741
         * @return void
742
         */
743
        protected function markAllVariablesRead(File $phpcsFile, $stackPtr)
4✔
744
        {
745
                $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr);
4✔
746
                if ($currScope === null) {
4✔
747
                        return;
×
748
                }
749
                $scopeInfo = $this->getOrCreateScopeInfo($currScope);
4✔
750
                $count = count($scopeInfo->variables);
4✔
751
                Helpers::debug("marking all $count variables in scope as read");
4✔
752
                foreach ($scopeInfo->variables as $varInfo) {
4✔
753
                        $this->markVariableRead($varInfo->name, $stackPtr, $scopeInfo->scopeStartIndex);
4✔
754
                }
2✔
755
        }
2✔
756

757
        /**
758
         * Process a parameter definition if it is inside a function definition.
759
         *
760
         * This does not include variables imported by a "use" statement.
761
         *
762
         * @param File   $phpcsFile
763
         * @param int    $stackPtr
764
         * @param string $varName
765
         * @param int    $outerScope
766
         *
767
         * @return void
768
         */
769
        protected function processVariableAsFunctionParameter(File $phpcsFile, $stackPtr, $varName, $outerScope)
228✔
770
        {
771
                Helpers::debug('processVariableAsFunctionParameter', $stackPtr, $varName);
228✔
772
                $tokens = $phpcsFile->getTokens();
228✔
773

774
                $functionPtr = Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
228✔
775
                if (! is_int($functionPtr)) {
228✔
776
                        throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
777
                }
778

779
                Helpers::debug('processVariableAsFunctionParameter found function definition', $tokens[$functionPtr]);
228✔
780
                $this->markVariableDeclaration($varName, ScopeType::PARAM, null, $stackPtr, $functionPtr);
228✔
781

782
                // Are we pass-by-reference?
783
                $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
228✔
784
                if (($referencePtr !== false) && ($tokens[$referencePtr]['code'] === T_BITWISE_AND)) {
228✔
785
                        Helpers::debug('processVariableAsFunctionParameter found pass-by-reference to scope', $outerScope);
28✔
786
                        $varInfo = $this->getOrCreateVariableInfo($varName, $functionPtr);
28✔
787
                        $varInfo->referencedVariableScope = $outerScope;
28✔
788
                }
14✔
789

790
                //  Are we optional with a default?
791
                if (Helpers::getNextAssignPointer($phpcsFile, $stackPtr) !== null) {
228✔
792
                        Helpers::debug('processVariableAsFunctionParameter optional with default');
16✔
793
                        $this->markVariableAssignment($varName, $stackPtr, $functionPtr);
16✔
794
                }
8✔
795

796
                // Are we using constructor promotion? If so, that counts as both definition and use.
797
                if (Helpers::isConstructorPromotion($phpcsFile, $stackPtr)) {
228✔
798
                        Helpers::debug('processVariableAsFunctionParameter constructor promotion');
12✔
799
                        $this->markVariableRead($varName, $stackPtr, $outerScope);
12✔
800
                }
6✔
801
        }
114✔
802

803
        /**
804
         * Process a variable definition if it is inside a function's "use" import.
805
         *
806
         * @param File   $phpcsFile
807
         * @param int    $stackPtr
808
         * @param string $varName
809
         * @param int    $outerScope The start of the scope outside the function definition
810
         *
811
         * @return void
812
         */
813
        protected function processVariableAsUseImportDefinition(File $phpcsFile, $stackPtr, $varName, $outerScope)
24✔
814
        {
815
                $tokens = $phpcsFile->getTokens();
24✔
816

817
                Helpers::debug('processVariableAsUseImportDefinition', $stackPtr, $varName, $outerScope);
24✔
818

819
                $endOfArgsPtr = $phpcsFile->findPrevious([T_CLOSE_PARENTHESIS], $stackPtr - 1, null);
24✔
820
                if (! is_int($endOfArgsPtr)) {
24✔
821
                        throw new \Exception("Arguments index not found for function use index {$stackPtr} when processing variable {$varName}");
×
822
                }
823
                $functionPtr = Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $endOfArgsPtr);
24✔
824
                if (! is_int($functionPtr)) {
24✔
825
                        throw new \Exception("Function index not found for function use index {$stackPtr} (using {$endOfArgsPtr}) when processing variable {$varName}");
×
826
                }
827

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

831
                // If it's undefined in the enclosing scope, the use is wrong
832
                if ($this->isVariableUndefined($varName, $stackPtr, $outerScope) === true) {
24✔
833
                        Helpers::debug("variable '{$varName}' in function definition looks undefined in scope", $outerScope);
8✔
834
                        $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
8✔
835
                        return;
8✔
836
                }
837

838
                $this->markVariableDeclaration($varName, ScopeType::BOUND, null, $stackPtr, $functionPtr);
24✔
839
                $this->markVariableAssignment($varName, $stackPtr, $functionPtr);
24✔
840

841
                // Are we pass-by-reference? If so, then any assignment to the variable in
842
                // the function scope also should count for the enclosing scope.
843
                $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
24✔
844
                if (is_int($referencePtr) && $tokens[$referencePtr]['code'] === T_BITWISE_AND) {
24✔
845
                        Helpers::debug("variable '{$varName}' in function definition looks passed by reference");
12✔
846
                        $varInfo = $this->getOrCreateVariableInfo($varName, $functionPtr);
12✔
847
                        $varInfo->referencedVariableScope = $outerScope;
12✔
848
                }
6✔
849
        }
12✔
850

851
        /**
852
         * Process a class property that is being defined.
853
         *
854
         * Property definitions are ignored currently because all property access is
855
         * legal, even to undefined properties.
856
         *
857
         * Can be called for any token and will return false if the variable is not
858
         * of this type.
859
         *
860
         * @param File $phpcsFile
861
         * @param int  $stackPtr
862
         *
863
         * @return bool
864
         */
865
        protected function processVariableAsClassProperty(File $phpcsFile, $stackPtr)
340✔
866
        {
867
                // Make sure we are not in a class method before assuming it's a property.
868
                $tokens = $phpcsFile->getTokens();
340✔
869

870
                /** @var array{conditions?: (int|string)[], content?: string}|null */
871
                $token = $tokens[$stackPtr];
340✔
872
                if ($token && !empty($token['conditions']) && !empty($token['content']) && !Helpers::areConditionsWithinFunctionBeforeClass($token)) {
340✔
873
                        return Helpers::areAnyConditionsAClass($token);
16✔
874
                }
875
                return false;
340✔
876
        }
877

878
        /**
879
         * Process a variable that is being accessed inside a catch block.
880
         *
881
         * Can be called for any token and will return false if the variable is not
882
         * of this type.
883
         *
884
         * @param File   $phpcsFile
885
         * @param int    $stackPtr
886
         * @param string $varName
887
         * @param int    $currScope
888
         *
889
         * @return bool
890
         */
891
        protected function processVariableAsCatchBlock(File $phpcsFile, $stackPtr, $varName, $currScope)
348✔
892
        {
893
                $tokens = $phpcsFile->getTokens();
348✔
894

895
                // Are we a catch block parameter?
896
                $openPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
348✔
897
                if ($openPtr === null) {
348✔
898
                        return false;
344✔
899
                }
900

901
                $catchPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
256✔
902
                if (($catchPtr !== false) && ($tokens[$catchPtr]['code'] === T_CATCH)) {
256✔
903
                        // Scope of the exception var is actually the function, not just the catch block.
904
                        $this->markVariableDeclaration($varName, ScopeType::LOCAL, null, $stackPtr, $currScope, true);
44✔
905
                        $this->markVariableAssignment($varName, $stackPtr, $currScope);
44✔
906
                        if ($this->allowUnusedCaughtExceptions) {
44✔
907
                                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
40✔
908
                                $varInfo->ignoreUnused = true;
40✔
909
                        }
20✔
910
                        return true;
44✔
911
                }
912
                return false;
212✔
913
        }
914

915
        /**
916
         * Process a variable that is being accessed as a member of `$this`.
917
         *
918
         * Looks for variables of the form `$this->myVariable`.
919
         *
920
         * Can be called for any token and will return false if the variable is not
921
         * of this type.
922
         *
923
         * @param File   $phpcsFile
924
         * @param int    $stackPtr
925
         * @param string $varName
926
         *
927
         * @return bool
928
         */
929
        protected function processVariableAsThisWithinClass(File $phpcsFile, $stackPtr, $varName)
348✔
930
        {
931
                $tokens = $phpcsFile->getTokens();
348✔
932
                $token  = $tokens[$stackPtr];
348✔
933

934
                // Are we $this within a class?
935
                if (($varName !== 'this') || empty($token['conditions'])) {
348✔
936
                        return false;
332✔
937
                }
938

939
                // Handle enums specially since their condition may not exist in old phpcs.
940
                $inEnum = false;
80✔
941
                foreach ($this->enums as $enum) {
80✔
942
                        if ($stackPtr > $enum->blockStart && $stackPtr < $enum->blockEnd) {
4✔
943
                                $inEnum = true;
4✔
944
                        }
2✔
945
                }
40✔
946

947
                $inFunction = false;
80✔
948
                foreach (array_reverse($token['conditions'], true) as $scopeCode) {
80✔
949
                        //  $this within a closure is valid
950
                        if ($scopeCode === T_CLOSURE && $inFunction === false) {
80✔
951
                                return true;
12✔
952
                        }
953

954
                        $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
80✔
955
                        if (defined('T_ENUM')) {
80✔
956
                                $classlikeCodes[] = T_ENUM;
40✔
957
                        }
20✔
958
                        if (in_array($scopeCode, $classlikeCodes, true)) {
80✔
959
                                return true;
72✔
960
                        }
961

962
                        if ($scopeCode === T_FUNCTION && $inEnum) {
80✔
963
                                return true;
4✔
964
                        }
965

966
                        // Handle nested function declarations.
967
                        if ($scopeCode === T_FUNCTION) {
80✔
968
                                if ($inFunction === true) {
80✔
969
                                        break;
4✔
970
                                }
971

972
                                $inFunction = true;
80✔
973
                        }
40✔
974
                }
40✔
975

976
                return false;
12✔
977
        }
978

979
        /**
980
         * Process a superglobal variable that is being accessed.
981
         *
982
         * Can be called for any token and will return false if the variable is not
983
         * of this type.
984
         *
985
         * @param string $varName
986
         *
987
         * @return bool
988
         */
989
        protected function processVariableAsSuperGlobal($varName)
340✔
990
        {
991
                $superglobals = [
170✔
992
                        'GLOBALS',
340✔
993
                        '_SERVER',
340✔
994
                        '_GET',
340✔
995
                        '_POST',
340✔
996
                        '_FILES',
340✔
997
                        '_COOKIE',
340✔
998
                        '_SESSION',
340✔
999
                        '_REQUEST',
340✔
1000
                        '_ENV',
340✔
1001
                        'argv',
340✔
1002
                        'argc',
340✔
1003
                        'http_response_header',
340✔
1004
                        'HTTP_RAW_POST_DATA',
340✔
1005
                ];
340✔
1006
                // Are we a superglobal variable?
1007
                return (in_array($varName, $superglobals, true));
340✔
1008
        }
1009

1010
        /**
1011
         * Process a variable that is being accessed with static syntax.
1012
         *
1013
         * That is, this will record the use of a variable of the form
1014
         * `MyClass::$myVariable` or `self::$myVariable`.
1015
         *
1016
         * Can be called for any token and will return false if the variable is not
1017
         * of this type.
1018
         *
1019
         * @param File $phpcsFile
1020
         * @param int  $stackPtr
1021
         *
1022
         * @return bool
1023
         */
1024
        protected function processVariableAsStaticMember(File $phpcsFile, $stackPtr)
340✔
1025
        {
1026
                $tokens = $phpcsFile->getTokens();
340✔
1027

1028
                $doubleColonPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
340✔
1029
                if ($doubleColonPtr === false || $tokens[$doubleColonPtr]['code'] !== T_DOUBLE_COLON) {
340✔
1030
                        return false;
340✔
1031
                }
1032
                $classNamePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $doubleColonPtr - 1, null, true);
28✔
1033
                $staticReferences = [
14✔
1034
                        T_STRING,
28✔
1035
                        T_SELF,
28✔
1036
                        T_PARENT,
28✔
1037
                        T_STATIC,
28✔
1038
                        T_VARIABLE,
28✔
1039
                ];
28✔
1040
                if ($classNamePtr === false || ! in_array($tokens[$classNamePtr]['code'], $staticReferences, true)) {
28✔
1041
                        return false;
×
1042
                }
1043
                // "When calling static methods, the function call is stronger than the
1044
                // static property operator" so look for a function call.
1045
                $parenPointer = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
28✔
1046
                if ($parenPointer !== false && $tokens[$parenPointer]['code'] === T_OPEN_PARENTHESIS) {
28✔
1047
                        return false;
4✔
1048
                }
1049
                return true;
28✔
1050
        }
1051

1052
        /**
1053
         * @param File   $phpcsFile
1054
         * @param int    $stackPtr
1055
         * @param string $varName
1056
         *
1057
         * @return bool
1058
         */
1059
        protected function processVariableAsStaticOutsideClass(File $phpcsFile, $stackPtr, $varName)
340✔
1060
        {
1061
                // Are we refering to self:: outside a class?
1062

1063
                $tokens = $phpcsFile->getTokens();
340✔
1064

1065
                /** @var array{conditions?: (int|string)[], content?: string}|null */
1066
                $token = $tokens[$stackPtr];
340✔
1067

1068
                $doubleColonPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
340✔
1069
                if ($doubleColonPtr === false || $tokens[$doubleColonPtr]['code'] !== T_DOUBLE_COLON) {
340✔
1070
                        return false;
340✔
1071
                }
1072
                $classNamePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $doubleColonPtr - 1, null, true);
28✔
1073
                if ($classNamePtr === false) {
28✔
1074
                        return false;
×
1075
                }
1076
                $code = $tokens[$classNamePtr]['code'];
28✔
1077
                $staticReferences = [
14✔
1078
                        T_SELF,
28✔
1079
                        T_STATIC,
28✔
1080
                ];
28✔
1081
                if (! in_array($code, $staticReferences, true)) {
28✔
1082
                        return false;
16✔
1083
                }
1084
                $errorClass = $code === T_SELF ? 'SelfOutsideClass' : 'StaticOutsideClass';
28✔
1085
                $staticRefType = $code === T_SELF ? 'self::' : 'static::';
28✔
1086
                if (!empty($token['conditions']) && !empty($token['content']) && Helpers::areAnyConditionsAClass($token)) {
28✔
1087
                        return false;
28✔
1088
                }
1089
                $phpcsFile->addError(
8✔
1090
                        "Use of {$staticRefType}%s outside class definition.",
8✔
1091
                        $stackPtr,
8✔
1092
                        $errorClass,
8✔
1093
                        ["\${$varName}"]
8✔
1094
                );
8✔
1095
                return true;
8✔
1096
        }
1097

1098
        /**
1099
         * Process a variable that is being assigned.
1100
         *
1101
         * This will record that the variable has been defined within a scope so that
1102
         * later we can determine if it it unused and we can guarantee that any
1103
         * future uses of the variable are not using an undefined variable.
1104
         *
1105
         * References (on either side of an assignment) behave differently and this
1106
         * function handles those cases as well.
1107
         *
1108
         * @param File   $phpcsFile
1109
         * @param int    $stackPtr
1110
         * @param string $varName
1111
         * @param int    $currScope
1112
         *
1113
         * @return void
1114
         */
1115
        protected function processVariableAsAssignment(File $phpcsFile, $stackPtr, $varName, $currScope)
320✔
1116
        {
1117
                Helpers::debug("processVariableAsAssignment: starting for '{$varName}'");
320✔
1118
                $assignPtr = Helpers::getNextAssignPointer($phpcsFile, $stackPtr);
320✔
1119
                if (! is_int($assignPtr)) {
320✔
1120
                        return;
×
1121
                }
1122

1123
                // If the right-hand-side of the assignment to this variable is a reference
1124
                // variable, then this variable is a reference to that one, and as such any
1125
                // assignment to this variable (except another assignment by reference,
1126
                // which would change the binding) has a side effect of changing the
1127
                // referenced variable and therefore should count as both an assignment and
1128
                // a read.
1129
                $tokens = $phpcsFile->getTokens();
320✔
1130
                $referencePtr = $phpcsFile->findNext(Tokens::$emptyTokens, $assignPtr + 1, null, true, null, true);
320✔
1131
                if (is_int($referencePtr) && $tokens[$referencePtr]['code'] === T_BITWISE_AND) {
320✔
1132
                        Helpers::debug("processVariableAsAssignment: found reference variable for '{$varName}'");
8✔
1133
                        $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
8✔
1134
                        // If the variable was already declared, but was not yet read, it is
1135
                        // unused because we're about to change the binding; that is, unless we
1136
                        // are inside a conditional block because in that case the condition may
1137
                        // never activate.
1138
                        $scopeInfo = $this->getOrCreateScopeInfo($currScope);
8✔
1139
                        $conditionPointer = Helpers::getClosestConditionPositionIfBeforeOtherConditions($tokens[$referencePtr]['conditions']);
8✔
1140
                        $lastAssignmentPtr = $varInfo->firstDeclared;
8✔
1141
                        if (! $conditionPointer && $lastAssignmentPtr) {
8✔
1142
                                Helpers::debug("processVariableAsAssignment: considering close of scope for '{$varName}' due to reference reassignment");
8✔
1143
                                $this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
8✔
1144
                        }
4✔
1145
                        if ($conditionPointer && $lastAssignmentPtr && $conditionPointer < $lastAssignmentPtr) {
8✔
1146
                                // We may be inside a condition but the last assignment was also inside this condition.
1147
                                Helpers::debug("processVariableAsAssignment: considering close of scope for '{$varName}' due to reference reassignment ignoring recent condition");
8✔
1148
                                $this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
8✔
1149
                        }
4✔
1150
                        if ($conditionPointer && $lastAssignmentPtr && $conditionPointer > $lastAssignmentPtr) {
8✔
1151
                                Helpers::debug("processVariableAsAssignment: not considering close of scope for '{$varName}' due to reference reassignment because it is conditional");
8✔
1152
                        }
4✔
1153
                        // The referenced variable may have a different name, but we don't
1154
                        // actually need to mark it as used in this case because the act of this
1155
                        // assignment will mark it used on the next token.
1156
                        $varInfo->referencedVariableScope = $currScope;
8✔
1157
                        $this->markVariableDeclaration($varName, ScopeType::LOCAL, null, $stackPtr, $currScope, true);
8✔
1158
                        // An assignment to a reference is a binding and should not count as
1159
                        // initialization since it doesn't change any values.
1160
                        $this->markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope);
8✔
1161
                        return;
8✔
1162
                }
1163

1164
                Helpers::debug('processVariableAsAssignment: marking as assignment in scope', $currScope);
320✔
1165
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
320✔
1166

1167
                // If the left-hand-side of the assignment (the variable we are examining)
1168
                // is itself a reference, then that counts as a read as well as a write.
1169
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
320✔
1170
                if ($varInfo->isDynamicReference) {
320✔
1171
                        Helpers::debug('processVariableAsAssignment: also marking as a use because variable is a reference');
16✔
1172
                        $this->markVariableRead($varName, $stackPtr, $currScope);
16✔
1173
                }
8✔
1174
        }
160✔
1175

1176
        /**
1177
         * Processes variables destructured from an array using shorthand list assignment.
1178
         *
1179
         * This will record the definition and assignment of variables defined using
1180
         * the format:
1181
         *
1182
         * ```
1183
         * [ $foo, $bar, $baz ] = $ary;
1184
         * ```
1185
         *
1186
         * Can be called for any token and will return false if the variable is not
1187
         * of this type.
1188
         *
1189
         * @param File   $phpcsFile
1190
         * @param int    $stackPtr
1191
         * @param string $varName
1192
         * @param int    $currScope
1193
         *
1194
         * @return bool
1195
         */
1196
        protected function processVariableAsListShorthandAssignment(File $phpcsFile, $stackPtr, $varName, $currScope)
332✔
1197
        {
1198
                // OK, are we within a [ ... ] construct?
1199
                $openPtr = Helpers::findContainingOpeningSquareBracket($phpcsFile, $stackPtr);
332✔
1200
                if (! is_int($openPtr)) {
332✔
1201
                        return false;
332✔
1202
                }
1203

1204
                // OK, we're a [ ... ] construct... are we being assigned to?
1205
                $assignments = Helpers::getListAssignments($phpcsFile, $openPtr);
48✔
1206
                if (! $assignments) {
48✔
1207
                        return false;
40✔
1208
                }
1209
                $matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) {
16✔
1210
                        if ($assignment === $stackPtr) {
32✔
1211
                                return $assignment;
32✔
1212
                        }
1213
                        return $thisAssignment;
24✔
1214
                });
32✔
1215
                if (! $matchingAssignment) {
32✔
1216
                        return false;
×
1217
                }
1218

1219
                // Yes, we're being assigned.
1220
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
32✔
1221
                return true;
32✔
1222
        }
1223

1224
        /**
1225
         * Processes variables destructured from an array using list assignment.
1226
         *
1227
         * This will record the definition and assignment of variables defined using
1228
         * the format:
1229
         *
1230
         * ```
1231
         * list( $foo, $bar, $baz ) = $ary;
1232
         * ```
1233
         *
1234
         * Can be called for any token and will return false if the variable is not
1235
         * of this type.
1236
         *
1237
         * @param File   $phpcsFile
1238
         * @param int    $stackPtr
1239
         * @param string $varName
1240
         * @param int    $currScope
1241
         *
1242
         * @return bool
1243
         */
1244
        protected function processVariableAsListAssignment(File $phpcsFile, $stackPtr, $varName, $currScope)
332✔
1245
        {
1246
                $tokens = $phpcsFile->getTokens();
332✔
1247

1248
                // OK, are we within a list (...) construct?
1249
                $openPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
332✔
1250
                if ($openPtr === null) {
332✔
1251
                        return false;
284✔
1252
                }
1253

1254
                $prevPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
212✔
1255
                if ((is_bool($prevPtr)) || ($tokens[$prevPtr]['code'] !== T_LIST)) {
212✔
1256
                        return false;
212✔
1257
                }
1258

1259
                // OK, we're a list (...) construct... are we being assigned to?
1260
                $assignments = Helpers::getListAssignments($phpcsFile, $prevPtr);
32✔
1261
                if (! $assignments) {
32✔
1262
                        return false;
8✔
1263
                }
1264
                $matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) {
32✔
1265
                        if ($assignment === $stackPtr) {
32✔
1266
                                return $assignment;
32✔
1267
                        }
1268
                        return $thisAssignment;
24✔
1269
                });
32✔
1270
                if (! $matchingAssignment) {
32✔
1271
                        return false;
×
1272
                }
1273

1274
                // Yes, we're being assigned.
1275
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
32✔
1276
                return true;
32✔
1277
        }
1278

1279
        /**
1280
         * Process a variable being defined (imported, really) with the `global` keyword.
1281
         *
1282
         * Can be called for any token and will return false if the variable is not
1283
         * of this type.
1284
         *
1285
         * @param File   $phpcsFile
1286
         * @param int    $stackPtr
1287
         * @param string $varName
1288
         * @param int    $currScope
1289
         *
1290
         * @return bool
1291
         */
1292
        protected function processVariableAsGlobalDeclaration(File $phpcsFile, $stackPtr, $varName, $currScope)
332✔
1293
        {
1294
                $tokens = $phpcsFile->getTokens();
332✔
1295

1296
                // Are we a global declaration?
1297
                // Search backwards for first token that isn't whitespace/comment, comma or variable.
1298
                $ignore             = Tokens::$emptyTokens;
332✔
1299
                $ignore[T_VARIABLE] = T_VARIABLE;
332✔
1300
                $ignore[T_COMMA]    = T_COMMA;
332✔
1301

1302
                $globalPtr = $phpcsFile->findPrevious($ignore, $stackPtr - 1, null, true, null, true);
332✔
1303
                if (($globalPtr === false) || ($tokens[$globalPtr]['code'] !== T_GLOBAL)) {
332✔
1304
                        return false;
332✔
1305
                }
1306

1307
                // It's a global declaration.
1308
                $this->markVariableDeclaration($varName, ScopeType::GLOBALSCOPE, null, $stackPtr, $currScope);
36✔
1309
                return true;
36✔
1310
        }
1311

1312
        /**
1313
         * Process a variable as a static declaration within a function.
1314
         *
1315
         * Specifically, this looks for variable definitions of the form `static
1316
         * $foo = 'hello';` or `static int $foo;` inside a function definition.
1317
         *
1318
         * This will not operate on variables that are written in a class definition
1319
         * outside of a function like `static $foo;` or `public static ?int $foo =
1320
         * 'bar';` because class properties (static or instance) are currently not
1321
         * tracked by this sniff. This is because a class property might be unused
1322
         * inside the class, but used outside the class (we cannot easily know if it
1323
         * is unused); this is also because it's common and legal to define class
1324
         * properties when they are assigned and that assignment can happen outside a
1325
         * class (we cannot easily know if the use of a property is undefined). These
1326
         * sorts of checks are better performed by static analysis tools that can see
1327
         * a whole project rather than a linter which can only easily see a file or
1328
         * some lines.
1329
         *
1330
         * If found, such a variable will be marked as declared (and possibly
1331
         * assigned, if it includes an initial value) within the scope of the
1332
         * function body.
1333
         *
1334
         * This will not operate on variables that use late static binding
1335
         * (`static::$foobar`) or the parameters of static methods even though they
1336
         * include the word `static` in the same statement.
1337
         *
1338
         * This only finds the defintions of static variables. Their use is handled
1339
         * by `processVariableAsStaticMember()`.
1340
         *
1341
         * Can be called for any token and will return false if the variable is not
1342
         * of this type.
1343
         *
1344
         * @param File   $phpcsFile
1345
         * @param int    $stackPtr
1346
         * @param string $varName
1347
         * @param int    $currScope
1348
         *
1349
         * @return bool
1350
         */
1351
        protected function processVariableAsStaticDeclaration(File $phpcsFile, $stackPtr, $varName, $currScope)
332✔
1352
        {
1353
                $tokens = $phpcsFile->getTokens();
332✔
1354

1355
                // Search backwards for a `static` keyword that occurs before the start of the statement.
1356
                $startOfStatement = $phpcsFile->findPrevious([T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_FN_ARROW, T_OPEN_PARENTHESIS], $stackPtr - 1, null, false, null, true);
332✔
1357
                $staticPtr = $phpcsFile->findPrevious([T_STATIC], $stackPtr - 1, null, false, null, true);
332✔
1358
                if (! is_int($startOfStatement)) {
332✔
1359
                        $startOfStatement = 1;
12✔
1360
                }
6✔
1361
                if (! is_int($staticPtr)) {
332✔
1362
                        return false;
328✔
1363
                }
1364
                // PHPCS is bad at finding the start of statements so we have to do it ourselves.
1365
                if ($staticPtr < $startOfStatement) {
60✔
1366
                        return false;
32✔
1367
                }
1368

1369
                // Is the 'static' keyword an anonymous static function declaration? If so,
1370
                // this is not a static variable declaration.
1371
                $tokenAfterStatic = $phpcsFile->findNext(Tokens::$emptyTokens, $staticPtr + 1, null, true, null, true);
60✔
1372
                $functionTokenTypes = [
30✔
1373
                        T_FUNCTION,
60✔
1374
                        T_CLOSURE,
60✔
1375
                        T_FN,
60✔
1376
                ];
60✔
1377
                if (is_int($tokenAfterStatic) && in_array($tokens[$tokenAfterStatic]['code'], $functionTokenTypes, true)) {
60✔
1378
                        return false;
16✔
1379
                }
1380

1381
                // Is the token inside function parameters? If so, this is not a static
1382
                // declaration because we must be inside a function body.
1383
                if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
52✔
1384
                        return false;
×
1385
                }
1386

1387
                // Is the token inside a function call? If so, this is not a static
1388
                // declaration.
1389
                if (Helpers::isTokenInsideFunctionCallArgument($phpcsFile, $stackPtr)) {
52✔
1390
                        return false;
12✔
1391
                }
1392

1393
                // Is the keyword a late static binding? If so, this isn't the static
1394
                // keyword we're looking for, but since static:: isn't allowed in a
1395
                // compile-time constant, we also know we can't be part of a static
1396
                // declaration anyway, so there's no need to look any further.
1397
                $lateStaticBindingPtr = $phpcsFile->findNext(T_WHITESPACE, $staticPtr + 1, null, true, null, true);
40✔
1398
                if (($lateStaticBindingPtr !== false) && ($tokens[$lateStaticBindingPtr]['code'] === T_DOUBLE_COLON)) {
40✔
1399
                        return false;
4✔
1400
                }
1401

1402
                $this->markVariableDeclaration($varName, ScopeType::STATICSCOPE, null, $stackPtr, $currScope);
36✔
1403
                if (Helpers::getNextAssignPointer($phpcsFile, $stackPtr) !== null) {
36✔
1404
                        $this->markVariableAssignment($varName, $stackPtr, $currScope);
×
1405
                }
1406
                return true;
36✔
1407
        }
1408

1409
        /**
1410
         * @param File   $phpcsFile
1411
         * @param int    $stackPtr
1412
         * @param string $varName
1413
         * @param int    $currScope
1414
         *
1415
         * @return bool
1416
         */
1417
        protected function processVariableAsForeachLoopVar(File $phpcsFile, $stackPtr, $varName, $currScope)
328✔
1418
        {
1419
                $tokens = $phpcsFile->getTokens();
328✔
1420

1421
                // Are we a foreach loopvar?
1422
                $openParenPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
328✔
1423
                if (! is_int($openParenPtr)) {
328✔
1424
                        return false;
280✔
1425
                }
1426
                $foreachPtr = Helpers::getIntOrNull($phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true));
212✔
1427
                if (! is_int($foreachPtr)) {
212✔
1428
                        return false;
×
1429
                }
1430
                if ($tokens[$foreachPtr]['code'] === T_LIST) {
212✔
1431
                        $openParenPtr = Helpers::findContainingOpeningBracket($phpcsFile, $foreachPtr);
8✔
1432
                        if (! is_int($openParenPtr)) {
8✔
1433
                                return false;
×
1434
                        }
1435
                        $foreachPtr = Helpers::getIntOrNull($phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true));
8✔
1436
                        if (! is_int($foreachPtr)) {
8✔
1437
                                return false;
×
1438
                        }
1439
                }
4✔
1440
                if ($tokens[$foreachPtr]['code'] !== T_FOREACH) {
212✔
1441
                        return false;
184✔
1442
                }
1443

1444
                // Is there an 'as' token between us and the foreach?
1445
                if ($phpcsFile->findPrevious(T_AS, $stackPtr - 1, $openParenPtr) === false) {
60✔
1446
                        return false;
60✔
1447
                }
1448
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
60✔
1449
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
60✔
1450

1451
                // Is this the value of a key => value foreach?
1452
                if ($phpcsFile->findPrevious(T_DOUBLE_ARROW, $stackPtr - 1, $openParenPtr) !== false) {
60✔
1453
                        $varInfo->isForeachLoopAssociativeValue = true;
20✔
1454
                }
10✔
1455

1456
                // Are we pass-by-reference?
1457
                $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
60✔
1458
                if (($referencePtr !== false) && ($tokens[$referencePtr]['code'] === T_BITWISE_AND)) {
60✔
1459
                        Helpers::debug('processVariableAsForeachLoopVar: found foreach loop variable assigned by reference');
24✔
1460
                        $varInfo->isDynamicReference = true;
24✔
1461
                }
12✔
1462

1463
                return true;
60✔
1464
        }
1465

1466
        /**
1467
         * @param File   $phpcsFile
1468
         * @param int    $stackPtr
1469
         * @param string $varName
1470
         * @param int    $currScope
1471
         *
1472
         * @return bool
1473
         */
1474
        protected function processVariableAsPassByReferenceFunctionCall(File $phpcsFile, $stackPtr, $varName, $currScope)
328✔
1475
        {
1476
                $tokens = $phpcsFile->getTokens();
328✔
1477

1478
                // Are we pass-by-reference to known pass-by-reference function?
1479
                $functionPtr = Helpers::findFunctionCall($phpcsFile, $stackPtr);
328✔
1480
                if ($functionPtr === null || ! isset($tokens[$functionPtr])) {
328✔
1481
                        return false;
314✔
1482
                }
1483

1484
                // Is our function a known pass-by-reference function?
1485
                $functionName = $tokens[$functionPtr]['content'];
158✔
1486
                $refArgs = $this->getPassByReferenceFunction($functionName);
158✔
1487
                if (! $refArgs) {
158✔
1488
                        return false;
154✔
1489
                }
1490

1491
                $argPtrs = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
20✔
1492

1493
                // We're within a function call arguments list, find which arg we are.
1494
                $argPos = false;
20✔
1495
                foreach ($argPtrs as $idx => $ptrs) {
20✔
1496
                        if (in_array($stackPtr, $ptrs)) {
20✔
1497
                                $argPos = $idx + 1;
20✔
1498
                                break;
20✔
1499
                        }
1500
                }
10✔
1501
                if ($argPos === false) {
20✔
1502
                        return false;
×
1503
                }
1504
                if (!in_array($argPos, $refArgs)) {
20✔
1505
                        // Our arg wasn't mentioned explicitly, are we after an elipsis catch-all?
1506
                        $elipsis = array_search('...', $refArgs);
20✔
1507
                        if ($elipsis === false) {
20✔
1508
                                return false;
20✔
1509
                        }
1510
                        $elipsis = (int)$elipsis;
16✔
1511
                        if ($argPos < $refArgs[$elipsis - 1]) {
16✔
1512
                                return false;
16✔
1513
                        }
1514
                }
8✔
1515

1516
                // Our argument position matches that of a pass-by-ref argument,
1517
                // check that we're the only part of the argument expression.
1518
                foreach ($argPtrs[$argPos - 1] as $ptr) {
16✔
1519
                        if ($ptr === $stackPtr) {
16✔
1520
                                continue;
16✔
1521
                        }
1522
                        if (isset(Tokens::$emptyTokens[$tokens[$ptr]['code']]) === false) {
16✔
1523
                                return false;
×
1524
                        }
1525
                }
8✔
1526

1527
                // Just us, we can mark it as a write.
1528
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
16✔
1529
                // It's a read as well for purposes of used-variables.
1530
                $this->markVariableRead($varName, $stackPtr, $currScope);
16✔
1531
                return true;
16✔
1532
        }
1533

1534
        /**
1535
         * @param File   $phpcsFile
1536
         * @param int    $stackPtr
1537
         * @param string $varName
1538
         * @param int    $currScope
1539
         *
1540
         * @return bool
1541
         */
1542
        protected function processVariableAsSymbolicObjectProperty(File $phpcsFile, $stackPtr, $varName, $currScope)
348✔
1543
        {
1544
                $tokens = $phpcsFile->getTokens();
348✔
1545

1546
                // Are we a symbolic object property/function derefeference?
1547
                // Search backwards for first token that isn't whitespace, is it a "->" operator?
1548
                $objectOperatorPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
348✔
1549
                if (($objectOperatorPtr === false) || ($tokens[$objectOperatorPtr]['code'] !== T_OBJECT_OPERATOR)) {
348✔
1550
                        return false;
348✔
1551
                }
1552

1553
                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
28✔
1554
                return true;
28✔
1555
        }
1556

1557
        /**
1558
         * Process a normal variable in the code.
1559
         *
1560
         * Most importantly, this function determines if the variable use is a "read"
1561
         * (using the variable for something) or a "write" (an assignment) or,
1562
         * sometimes, both at once.
1563
         *
1564
         * It also determines the scope of the variable (where it begins and ends).
1565
         *
1566
         * Using these two pieces of information, we can determine if the variable is
1567
         * being used ("read") without having been defined ("write").
1568
         *
1569
         * We can also determine, once the scan has hit the end of a scope, if any of
1570
         * the variables within that scope have been defined ("write") without being
1571
         * used ("read"). That behavior, however, happens in the `processScopeClose()`
1572
         * function using the data gathered by this function.
1573
         *
1574
         * Some variables are used in more complex ways, so there are other similar
1575
         * functions to this one, like `processVariableInString`, and
1576
         * `processCompact`. They have the same purpose as this function, though.
1577
         *
1578
         * If the 'ignore-for-loops' option is true, we will ignore the special
1579
         * processing for the increment variables of for loops. This will prevent
1580
         * infinite loops.
1581
         *
1582
         * @param File                           $phpcsFile The PHP_CodeSniffer file where this token was found.
1583
         * @param int                            $stackPtr  The position where the token was found.
1584
         * @param array<string, bool|string|int> $options   See above.
1585
         *
1586
         * @return void
1587
         */
1588
        protected function processVariable(File $phpcsFile, $stackPtr, $options = [])
352✔
1589
        {
1590
                $tokens = $phpcsFile->getTokens();
352✔
1591
                $token  = $tokens[$stackPtr];
352✔
1592

1593
                // Get the name of the variable.
1594
                $varName = Helpers::normalizeVarName($token['content']);
352✔
1595
                Helpers::debug("examining token for variable '{$varName}' on line {$token['line']}", $token);
352✔
1596

1597
                // Find the start of the current scope.
1598
                $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr);
352✔
1599
                if ($currScope === null) {
352✔
1600
                        Helpers::debug('no scope found');
52✔
1601
                        return;
52✔
1602
                }
1603
                Helpers::debug("start of scope for variable '{$varName}' is", $currScope);
348✔
1604

1605
                // Determine if variable is being assigned ("write") or used ("read").
1606

1607
                // Read methods that preempt assignment:
1608
                //   Are we a $object->$property type symbolic reference?
1609

1610
                // Possible assignment methods:
1611
                //   Is a mandatory function/closure parameter
1612
                //   Is an optional function/closure parameter with non-null value
1613
                //   Is closure use declaration of a variable defined within containing scope
1614
                //   catch (...) block start
1615
                //   $this within a class.
1616
                //   $GLOBALS, $_REQUEST, etc superglobals.
1617
                //   $var part of class::$var static member
1618
                //   Assignment via =
1619
                //   Assignment via list (...) =
1620
                //   Declares as a global
1621
                //   Declares as a static
1622
                //   Assignment via foreach (... as ...) { }
1623
                //   Pass-by-reference to known pass-by-reference function
1624

1625
                // Are we inside the third expression of a for loop? Store such variables
1626
                // for processing after the loop ends by `processClosingForLoopsAt()`.
1627
                if (empty($options['ignore-for-loops'])) {
348✔
1628
                        $forLoop = Helpers::getForLoopForIncrementVariable($stackPtr, $this->forLoops);
348✔
1629
                        if ($forLoop) {
348✔
1630
                                Helpers::debug('found variable inside for loop third expression');
8✔
1631
                                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
8✔
1632
                                $forLoop->incrementVariables[$stackPtr] = $varInfo;
8✔
1633
                                return;
8✔
1634
                        }
1635
                }
174✔
1636

1637
                // Are we a $object->$property type symbolic reference?
1638
                if ($this->processVariableAsSymbolicObjectProperty($phpcsFile, $stackPtr, $varName, $currScope)) {
348✔
1639
                        Helpers::debug('found symbolic object property');
28✔
1640
                        return;
28✔
1641
                }
1642

1643
                // Are we a function or closure parameter?
1644
                if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
348✔
1645
                        Helpers::debug('found function definition parameter');
228✔
1646
                        $this->processVariableAsFunctionParameter($phpcsFile, $stackPtr, $varName, $currScope);
228✔
1647
                        return;
228✔
1648
                }
1649

1650
                // Are we a variable being imported into a function's scope with "use"?
1651
                if (Helpers::isTokenInsideFunctionUseImport($phpcsFile, $stackPtr)) {
348✔
1652
                        Helpers::debug('found use scope import definition');
24✔
1653
                        $this->processVariableAsUseImportDefinition($phpcsFile, $stackPtr, $varName, $currScope);
24✔
1654
                        return;
24✔
1655
                }
1656

1657
                // Are we a catch parameter?
1658
                if ($this->processVariableAsCatchBlock($phpcsFile, $stackPtr, $varName, $currScope)) {
348✔
1659
                        Helpers::debug('found catch block');
44✔
1660
                        return;
44✔
1661
                }
1662

1663
                // Are we $this within a class?
1664
                if ($this->processVariableAsThisWithinClass($phpcsFile, $stackPtr, $varName)) {
348✔
1665
                        Helpers::debug('found this usage within a class');
72✔
1666
                        return;
72✔
1667
                }
1668

1669
                // Are we a $GLOBALS, $_REQUEST, etc superglobal?
1670
                if ($this->processVariableAsSuperGlobal($varName)) {
340✔
1671
                        Helpers::debug('found superglobal');
12✔
1672
                        return;
12✔
1673
                }
1674

1675
                // Check for static members used outside a class
1676
                if ($this->processVariableAsStaticOutsideClass($phpcsFile, $stackPtr, $varName)) {
340✔
1677
                        Helpers::debug('found static usage outside of class');
8✔
1678
                        return;
8✔
1679
                }
1680

1681
                // $var part of class::$var static member
1682
                if ($this->processVariableAsStaticMember($phpcsFile, $stackPtr)) {
340✔
1683
                        Helpers::debug('found static member');
28✔
1684
                        return;
28✔
1685
                }
1686

1687
                if ($this->processVariableAsClassProperty($phpcsFile, $stackPtr)) {
340✔
1688
                        Helpers::debug('found class property definition');
4✔
1689
                        return;
4✔
1690
                }
1691

1692
                // Is the next non-whitespace an assignment?
1693
                if (Helpers::isTokenInsideAssignmentLHS($phpcsFile, $stackPtr)) {
340✔
1694
                        Helpers::debug('found assignment');
320✔
1695
                        $this->processVariableAsAssignment($phpcsFile, $stackPtr, $varName, $currScope);
320✔
1696
                        if (Helpers::isTokenInsideAssignmentRHS($phpcsFile, $stackPtr) || Helpers::isTokenInsideFunctionCallArgument($phpcsFile, $stackPtr)) {
320✔
1697
                                Helpers::debug("found assignment that's also inside an expression");
12✔
1698
                                $this->markVariableRead($varName, $stackPtr, $currScope);
12✔
1699
                                return;
12✔
1700
                        }
1701
                        return;
320✔
1702
                }
1703

1704
                // OK, are we within a list (...) = construct?
1705
                if ($this->processVariableAsListAssignment($phpcsFile, $stackPtr, $varName, $currScope)) {
332✔
1706
                        Helpers::debug('found list assignment');
32✔
1707
                        return;
32✔
1708
                }
1709

1710
                // OK, are we within a [...] = construct?
1711
                if ($this->processVariableAsListShorthandAssignment($phpcsFile, $stackPtr, $varName, $currScope)) {
332✔
1712
                        Helpers::debug('found list shorthand assignment');
32✔
1713
                        return;
32✔
1714
                }
1715

1716
                // Are we a global declaration?
1717
                if ($this->processVariableAsGlobalDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) {
332✔
1718
                        Helpers::debug('found global declaration');
36✔
1719
                        return;
36✔
1720
                }
1721

1722
                // Are we a static declaration?
1723
                if ($this->processVariableAsStaticDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) {
332✔
1724
                        Helpers::debug('found static declaration');
36✔
1725
                        return;
36✔
1726
                }
1727

1728
                // Are we a foreach loopvar?
1729
                if ($this->processVariableAsForeachLoopVar($phpcsFile, $stackPtr, $varName, $currScope)) {
328✔
1730
                        Helpers::debug('found foreach loop variable');
60✔
1731
                        return;
60✔
1732
                }
1733

1734
                // Are we pass-by-reference to known pass-by-reference function?
1735
                if ($this->processVariableAsPassByReferenceFunctionCall($phpcsFile, $stackPtr, $varName, $currScope)) {
328✔
1736
                        Helpers::debug('found pass by reference');
16✔
1737
                        return;
16✔
1738
                }
1739

1740
                // Are we a numeric variable used for constructs like preg_replace?
1741
                if (Helpers::isVariableANumericVariable($varName)) {
328✔
1742
                        Helpers::debug('found numeric variable');
×
1743
                        return;
×
1744
                }
1745

1746
                if (Helpers::isVariableInsideElseCondition($phpcsFile, $stackPtr) || Helpers::isVariableInsideElseBody($phpcsFile, $stackPtr)) {
328✔
1747
                        Helpers::debug('found variable inside else condition or body');
24✔
1748
                        $this->processVaribleInsideElse($phpcsFile, $stackPtr, $varName, $currScope);
24✔
1749
                        return;
24✔
1750
                }
1751

1752
                // Are we an isset or empty call?
1753
                if (Helpers::isVariableInsideIssetOrEmpty($phpcsFile, $stackPtr)) {
328✔
1754
                        Helpers::debug('found isset or empty');
20✔
1755
                        $this->markVariableRead($varName, $stackPtr, $currScope);
20✔
1756
                        return;
20✔
1757
                }
1758

1759
                // OK, we don't appear to be a write to the var, assume we're a read.
1760
                Helpers::debug('looks like a variable read');
328✔
1761
                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
328✔
1762
        }
164✔
1763

1764
        /**
1765
         * @param File   $phpcsFile
1766
         * @param int    $stackPtr
1767
         * @param string $varName
1768
         * @param int    $currScope
1769
         *
1770
         * @return void
1771
         */
1772
        protected function processVaribleInsideElse(File $phpcsFile, $stackPtr, $varName, $currScope)
24✔
1773
        {
1774
                // Find all assignments to this variable inside the current scope.
1775
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
24✔
1776
                $allAssignmentIndices = array_unique($varInfo->allAssignments);
24✔
1777
                // Find the attached 'if' and 'elseif' block start and end indices.
1778
                $blockIndices = Helpers::getAttachedBlockIndicesForElse($phpcsFile, $stackPtr);
24✔
1779

1780
                // If all of the assignments are within the previous attached blocks, then warn about undefined.
1781
                $tokens = $phpcsFile->getTokens();
24✔
1782
                $assignmentsInsideAttachedBlocks = [];
24✔
1783
                foreach ($allAssignmentIndices as $index) {
24✔
1784
                        foreach ($blockIndices as $blockIndex) {
24✔
1785
                                $blockToken = $tokens[$blockIndex];
24✔
1786
                                Helpers::debug('for variable inside else, looking at assignment', $index, 'at block index', $blockIndex, 'which is token', $blockToken);
24✔
1787
                                if (isset($blockToken['scope_opener']) && isset($blockToken['scope_closer'])) {
24✔
1788
                                        $scopeOpener = $blockToken['scope_opener'];
16✔
1789
                                        $scopeCloser = $blockToken['scope_closer'];
16✔
1790
                                } else {
8✔
1791
                                        // If the `if` statement has no scope, it is probably inline, which
1792
                                        // means its scope is from the end of the condition up until the next
1793
                                        // semicolon
1794
                                        $scopeOpener = isset($blockToken['parenthesis_closer']) ? $blockToken['parenthesis_closer'] : $blockIndex + 1;
8✔
1795
                                        $scopeCloser = $phpcsFile->findNext([T_SEMICOLON], $scopeOpener);
8✔
1796
                                        if (! $scopeCloser) {
8✔
1797
                                                throw new \Exception("Cannot find scope for if condition block at index {$stackPtr} while examining variable {$varName}");
×
1798
                                        }
1799
                                }
1800
                                Helpers::debug('for variable inside else, looking at scope', $index, 'between', $scopeOpener, 'and', $scopeCloser);
24✔
1801
                                if (Helpers::isIndexInsideScope($index, $scopeOpener, $scopeCloser)) {
24✔
1802
                                        $assignmentsInsideAttachedBlocks[] = $index;
16✔
1803
                                }
8✔
1804
                        }
12✔
1805
                }
12✔
1806

1807
                if (count($assignmentsInsideAttachedBlocks) === count($allAssignmentIndices)) {
24✔
1808
                        if (! $varInfo->ignoreUndefined) {
16✔
1809
                                Helpers::debug("variable $varName inside else looks undefined");
16✔
1810
                                $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
16✔
1811
                        }
8✔
1812
                        return;
16✔
1813
                }
1814

1815
                Helpers::debug('looks like a variable read inside else');
24✔
1816
                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
24✔
1817
        }
12✔
1818

1819
        /**
1820
         * Called to process variables found in double quoted strings.
1821
         *
1822
         * Note that there may be more than one variable in the string, which will
1823
         * result only in one call for the string.
1824
         *
1825
         * @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
1826
         * @param int  $stackPtr  The position where the double quoted string was found.
1827
         *
1828
         * @return void
1829
         */
1830
        protected function processVariableInString(File $phpcsFile, $stackPtr)
164✔
1831
        {
1832
                $tokens = $phpcsFile->getTokens();
164✔
1833
                $token  = $tokens[$stackPtr];
164✔
1834

1835
                $regexp = Constants::getDoubleQuotedVarRegexp();
164✔
1836
                if (! empty($regexp) && !preg_match_all($regexp, $token['content'], $matches)) {
164✔
1837
                        Helpers::debug('processVariableInString: no variables found', $token);
24✔
1838
                        return;
24✔
1839
                }
1840
                Helpers::debug('examining token for variable in string', $token);
148✔
1841

1842
                if (empty($matches)) {
148✔
1843
                        Helpers::debug('processVariableInString: no variables found after search', $token);
×
1844
                        return;
×
1845
                }
1846
                foreach ($matches[1] as $varName) {
148✔
1847
                        $varName = Helpers::normalizeVarName($varName);
148✔
1848

1849
                        // Are we $this within a class?
1850
                        if ($this->processVariableAsThisWithinClass($phpcsFile, $stackPtr, $varName)) {
148✔
1851
                                continue;
8✔
1852
                        }
1853

1854
                        if ($this->processVariableAsSuperGlobal($varName)) {
140✔
1855
                                continue;
12✔
1856
                        }
1857

1858
                        // Are we a numeric variable used for constructs like preg_replace?
1859
                        if (Helpers::isVariableANumericVariable($varName)) {
140✔
1860
                                continue;
4✔
1861
                        }
1862

1863
                        $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr, $varName);
140✔
1864
                        if ($currScope === null) {
140✔
1865
                                continue;
×
1866
                        }
1867

1868
                        $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
140✔
1869
                }
74✔
1870
        }
74✔
1871

1872
        /**
1873
         * Called to process variables named in a call to compact().
1874
         *
1875
         * @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
1876
         * @param int  $stackPtr  The position where the call to compact() was found.
1877
         *
1878
         * @return void
1879
         */
1880
        protected function processCompact(File $phpcsFile, $stackPtr)
12✔
1881
        {
1882
                Helpers::debug("processCompact at {$stackPtr}");
12✔
1883
                $arguments = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
12✔
1884
                $variables = Helpers::getVariablesInsideCompact($phpcsFile, $stackPtr, $arguments);
12✔
1885
                foreach ($variables as $variable) {
12✔
1886
                        $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr, $variable->name);
12✔
1887
                        if ($currScope === null) {
12✔
1888
                                continue;
×
1889
                        }
1890
                        $variablePosition = $variable->firstRead ? $variable->firstRead : $stackPtr;
12✔
1891
                        $this->markVariableReadAndWarnIfUndefined($phpcsFile, $variable->name, $variablePosition, $currScope);
12✔
1892
                }
6✔
1893
        }
6✔
1894

1895
        /**
1896
         * Called to process the end of a scope.
1897
         *
1898
         * Note that although triggered by the closing curly brace of the scope,
1899
         * $stackPtr is the scope conditional, not the closing curly brace.
1900
         *
1901
         * @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
1902
         * @param int  $stackPtr  The position of the scope conditional.
1903
         *
1904
         * @return void
1905
         */
1906
        protected function processScopeClose(File $phpcsFile, $stackPtr)
352✔
1907
        {
1908
                Helpers::debug("processScopeClose at {$stackPtr}");
352✔
1909
                $scopeInfo = $this->scopeManager->getScopeForScopeStart($phpcsFile->getFilename(), $stackPtr);
352✔
1910
                if (is_null($scopeInfo)) {
352✔
1911
                        return;
×
1912
                }
1913
                foreach ($scopeInfo->variables as $varInfo) {
352✔
1914
                        $this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
340✔
1915
                }
176✔
1916
        }
176✔
1917

1918
        /**
1919
         * Warn about an unused variable if it has not been used within a scope.
1920
         *
1921
         * @param File         $phpcsFile
1922
         * @param VariableInfo $varInfo
1923
         * @param ScopeInfo    $scopeInfo
1924
         *
1925
         * @return void
1926
         */
1927
        protected function processScopeCloseForVariable(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
340✔
1928
        {
1929
                Helpers::debug('processScopeCloseForVariable', $varInfo);
340✔
1930
                if ($varInfo->ignoreUnused || isset($varInfo->firstRead)) {
340✔
1931
                        return;
336✔
1932
                }
1933
                if ($this->allowUnusedFunctionParameters && $varInfo->scopeType === ScopeType::PARAM) {
240✔
1934
                        return;
4✔
1935
                }
1936
                if ($this->allowUnusedParametersBeforeUsed && $varInfo->scopeType === ScopeType::PARAM && Helpers::areFollowingArgumentsUsed($varInfo, $scopeInfo)) {
236✔
1937
                        Helpers::debug("variable '{$varInfo->name}' at end of scope has unused following args");
16✔
1938
                        return;
16✔
1939
                }
1940
                if ($this->allowUnusedForeachVariables && $varInfo->isForeachLoopAssociativeValue) {
236✔
1941
                        return;
12✔
1942
                }
1943
                if ($varInfo->referencedVariableScope !== null && isset($varInfo->firstInitialized)) {
236✔
1944
                        Helpers::debug("variable '{$varInfo->name}' at end of scope is a reference and so counts as used");
32✔
1945
                        // If we're pass-by-reference then it's a common pattern to
1946
                        // use the variable to return data to the caller, so any
1947
                        // assignment also counts as "variable use" for the purposes
1948
                        // of "unused variable" warnings.
1949
                        return;
32✔
1950
                }
1951
                if ($varInfo->scopeType === ScopeType::GLOBALSCOPE && isset($varInfo->firstInitialized)) {
232✔
1952
                        Helpers::debug("variable '{$varInfo->name}' at end of scope is a global and so counts as used");
12✔
1953
                        // If we imported this variable from the global scope, any further use of
1954
                        // the variable, including assignment, should count as "variable use" for
1955
                        // the purposes of "unused variable" warnings.
1956
                        return;
12✔
1957
                }
1958
                if (empty($varInfo->firstDeclared) && empty($varInfo->firstInitialized)) {
232✔
1959
                        return;
16✔
1960
                }
1961
                if ($this->allowUnusedVariablesBeforeRequire && Helpers::isRequireInScopeAfter($phpcsFile, $varInfo, $scopeInfo)) {
232✔
1962
                        return;
4✔
1963
                }
1964
                if ($scopeInfo->scopeStartIndex === 0 && $this->allowUnusedVariablesInFileScope) {
228✔
1965
                        return;
4✔
1966
                }
1967
                if (
1968
                        ! empty($varInfo->firstDeclared)
224✔
1969
                        && $varInfo->scopeType === ScopeType::PARAM
224✔
1970
                        && Helpers::isInAbstractClass(
224✔
1971
                                $phpcsFile,
166✔
1972
                                Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $varInfo->firstDeclared) ?: 0
166✔
1973
                        )
166✔
1974
                        && Helpers::isFunctionBodyEmpty(
224✔
1975
                                $phpcsFile,
116✔
1976
                                Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $varInfo->firstDeclared) ?: 0
116✔
1977
                        )
116✔
1978
                ) {
112✔
1979
                        // Allow non-abstract methods inside an abstract class to have unused
1980
                        // parameters if the method body does nothing. Such methods are
1981
                        // effectively optional abstract methods so their unused parameters
1982
                        // should be ignored as we do with abstract method parameters.
1983
                        return;
8✔
1984
                }
1985

1986
                $this->warnAboutUnusedVariable($phpcsFile, $varInfo);
224✔
1987
        }
112✔
1988

1989
        /**
1990
         * Register warnings for a variable that is defined but not used.
1991
         *
1992
         * @param File         $phpcsFile
1993
         * @param VariableInfo $varInfo
1994
         *
1995
         * @return void
1996
         */
1997
        protected function warnAboutUnusedVariable(File $phpcsFile, VariableInfo $varInfo)
224✔
1998
        {
1999
                foreach (array_unique($varInfo->allAssignments) as $indexForWarning) {
224✔
2000
                        Helpers::debug("variable '{$varInfo->name}' at end of scope looks unused");
224✔
2001
                        $phpcsFile->addWarning(
224✔
2002
                                'Unused %s %s.',
224✔
2003
                                $indexForWarning,
224✔
2004
                                'UnusedVariable',
224✔
2005
                                [
112✔
2006
                                        VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType ?: ScopeType::LOCAL],
224✔
2007
                                        "\${$varInfo->name}",
224✔
2008
                                ]
112✔
2009
                        );
224✔
2010
                }
112✔
2011
        }
112✔
2012

2013
        /**
2014
         * @param File   $phpcsFile
2015
         * @param string $varName
2016
         * @param int    $stackPtr
2017
         *
2018
         * @return void
2019
         */
2020
        protected function warnAboutUndefinedVariable(File $phpcsFile, $varName, $stackPtr)
248✔
2021
        {
2022
                $phpcsFile->addWarning(
248✔
2023
                        'Variable %s is undefined.',
248✔
2024
                        $stackPtr,
248✔
2025
                        'UndefinedVariable',
248✔
2026
                        ["\${$varName}"]
248✔
2027
                );
248✔
2028
        }
124✔
2029

2030
        /**
2031
         * @param File   $phpcsFile
2032
         * @param string $varName
2033
         * @param int    $stackPtr
2034
         *
2035
         * @return void
2036
         */
2037
        protected function warnAboutUndefinedArrayPushShortcut(File $phpcsFile, $varName, $stackPtr)
28✔
2038
        {
2039
                $phpcsFile->addWarning(
28✔
2040
                        'Array variable %s is undefined.',
28✔
2041
                        $stackPtr,
28✔
2042
                        'UndefinedVariable',
28✔
2043
                        ["\${$varName}"]
28✔
2044
                );
28✔
2045
        }
14✔
2046

2047
        /**
2048
         * @param File   $phpcsFile
2049
         * @param string $varName
2050
         * @param int    $stackPtr
2051
         *
2052
         * @return void
2053
         */
2054
        protected function warnAboutUndefinedUnset(File $phpcsFile, $varName, $stackPtr)
8✔
2055
        {
2056
                $phpcsFile->addWarning(
8✔
2057
                        'Variable %s inside unset call is undefined.',
8✔
2058
                        $stackPtr,
8✔
2059
                        'UndefinedUnsetVariable',
8✔
2060
                        ["\${$varName}"]
8✔
2061
                );
8✔
2062
        }
4✔
2063
}
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