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

sirbrillig / phpcs-variable-analysis / 4406309569

pending completion
4406309569

push

github

GitHub
Allow non-enum tokens called 'enum' (#293)

5 of 5 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

1491 of 1615 relevant lines covered (92.32%)

138.33 hits per line

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

95.58
/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()
157
        {
158
                $this->scopeManager = new ScopeManager();
344✔
159
        }
172✔
160

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

191
        /**
192
         * @param string $functionName
193
         *
194
         * @return array<int|string>
195
         */
196
        private function getPassByReferenceFunction($functionName)
197
        {
198
                $passByRefFunctions = Constants::getPassByReferenceFunctions();
154✔
199
                if (!empty($this->sitePassByRefFunctions)) {
154✔
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) {
154✔
207
                        $passByRefFunctions = array_merge($passByRefFunctions, Constants::getWordPressPassByReferenceFunctions());
4✔
208
                }
2✔
209
                return isset($passByRefFunctions[$functionName]) ? $passByRefFunctions[$functionName] : [];
154✔
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)
224
        {
225
                $tokens = $phpcsFile->getTokens();
344✔
226

227
                $scopeStartTokenTypes = [
172✔
228
                        T_FUNCTION,
344✔
229
                        T_CLOSURE,
344✔
230
                ];
344✔
231

232
                $token = $tokens[$stackPtr];
344✔
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) {
344✔
237
                        $this->currentFile = $phpcsFile;
344✔
238
                        $this->forLoops = [];
344✔
239
                        $this->enums = [];
344✔
240
                }
172✔
241

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

248
                // Report variables defined but not used in the current scope as unused
249
                // variables if the current token closes scopes.
250
                $this->searchForAndProcessClosingScopesAt($phpcsFile, $stackPtr);
344✔
251

252
                // Scan variables that were postponed because they exist in the increment
253
                // expression of a for loop if the current token closes a loop.
254
                $this->processClosingForLoopsAt($phpcsFile, $stackPtr);
344✔
255

256
                // Find and process variables to perform two jobs: to record variable
257
                // definition or use, and to report variables as undefined if they are used
258
                // without having been first defined.
259
                if ($token['code'] === T_VARIABLE) {
344✔
260
                        $this->processVariable($phpcsFile, $stackPtr);
344✔
261
                        return;
344✔
262
                }
263
                if (($token['code'] === T_DOUBLE_QUOTED_STRING) || ($token['code'] === T_HEREDOC)) {
344✔
264
                        $this->processVariableInString($phpcsFile, $stackPtr);
164✔
265
                        return;
164✔
266
                }
267
                if (($token['code'] === T_STRING) && ($token['content'] === 'compact')) {
344✔
268
                        $this->processCompact($phpcsFile, $stackPtr);
12✔
269
                        return;
12✔
270
                }
271

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

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

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

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

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

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

338
                $tokens = $phpcsFile->getTokens();
344✔
339
                $token = $tokens[$stackPtr];
344✔
340
                $line = $token['line'];
344✔
341
                foreach ($scopeIndicesThisCloses as $scopeIndexThisCloses) {
344✔
342
                        Helpers::debug('found closing scope at index', $stackPtr, 'line', $line, 'for scopes starting at:', $scopeIndexThisCloses->scopeStartIndex);
344✔
343
                        $this->processScopeClose($phpcsFile, $scopeIndexThisCloses->scopeStartIndex);
344✔
344
                }
172✔
345
        }
172✔
346

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

364
                foreach ($forLoopsThisCloses as $forLoop) {
344✔
365
                        foreach ($forLoop->incrementVariables as $varIndex => $varInfo) {
8✔
366
                                Helpers::debug('processing delayed for loop increment variable at', $varIndex, $varInfo);
8✔
367
                                $this->processVariable($phpcsFile, $varIndex, ['ignore-for-loops' => true]);
8✔
368
                        }
4✔
369
                }
172✔
370
        }
172✔
371

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

395
        /**
396
         * @return string
397
         */
398
        protected function getFilename()
399
        {
400
                return $this->currentFile ? $this->currentFile->getFilename() : 'unknown file';
332✔
401
        }
402

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

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

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

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

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

528
                // Is the variable referencing another variable? If so, mark that variable used also.
529
                if ($varInfo->referencedVariableScope !== null && $varInfo->referencedVariableScope !== $currScope) {
316✔
530
                        // 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
531
                        if ($this->getVariableInfo($varInfo->name, $varInfo->referencedVariableScope)) {
20✔
532
                                Helpers::debug('markVariableAssignmentWithoutInitialization: marking referenced variable as assigned also', $varName);
4✔
533
                                $this->markVariableAssignment($varInfo->name, $stackPtr, $varInfo->referencedVariableScope);
4✔
534
                        }
2✔
535
                }
10✔
536

537
                if (empty($varInfo->scopeType)) {
316✔
538
                        $varInfo->scopeType = ScopeType::LOCAL;
308✔
539
                }
154✔
540
                $varInfo->allAssignments[] = $stackPtr;
316✔
541
        }
158✔
542

543
        /**
544
         * Record that a variable has been defined within a scope.
545
         *
546
         * @param string                                                                                           $varName
547
         * @param ScopeType::PARAM|ScopeType::BOUND|ScopeType::LOCAL|ScopeType::GLOBALSCOPE|ScopeType::STATICSCOPE $scopeType
548
         * @param ?string                                                                                          $typeHint
549
         * @param int                                                                                              $stackPtr
550
         * @param int                                                                                              $currScope
551
         * @param ?bool                                                                                            $permitMatchingRedeclaration
552
         *
553
         * @return void
554
         */
555
        protected function markVariableDeclaration(
556
                $varName,
557
                $scopeType,
558
                $typeHint,
559
                $stackPtr,
560
                $currScope,
561
                $permitMatchingRedeclaration = false
562
        ) {
563
                Helpers::debug("marking variable '{$varName}' declared in scope starting at token", $currScope);
252✔
564
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
252✔
565

566
                if (! empty($varInfo->scopeType)) {
252✔
567
                        if (($permitMatchingRedeclaration === false) || ($varInfo->scopeType !== $scopeType)) {
16✔
568
                                //  Issue redeclaration/reuse warning
569
                                //  Note: we check off scopeType not firstDeclared, this is so that
570
                                //    we catch declarations that come after implicit declarations like
571
                                //    use of a variable as a local.
572
                                $this->addWarning(
8✔
573
                                        'Redeclaration of %s %s as %s.',
8✔
574
                                        $stackPtr,
8✔
575
                                        'VariableRedeclaration',
8✔
576
                                        [
4✔
577
                                                VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType],
8✔
578
                                                "\${$varName}",
8✔
579
                                                VariableInfo::$scopeTypeDescriptions[$scopeType],
8✔
580
                                        ]
4✔
581
                                );
8✔
582
                        }
4✔
583
                }
8✔
584

585
                $varInfo->scopeType = $scopeType;
252✔
586
                if (isset($typeHint)) {
252✔
587
                        $varInfo->typeHint = $typeHint;
×
588
                }
589
                if (isset($varInfo->firstDeclared) && ($varInfo->firstDeclared <= $stackPtr)) {
252✔
590
                        Helpers::debug("variable '{$varName}' was already marked declared", $varInfo);
16✔
591
                        return;
16✔
592
                }
593
                $varInfo->firstDeclared = $stackPtr;
252✔
594
                $varInfo->allAssignments[] = $stackPtr;
252✔
595
                Helpers::debug("variable '{$varName}' marked declared", $varInfo);
252✔
596
        }
126✔
597

598
        /**
599
         * @param string   $message
600
         * @param int      $stackPtr
601
         * @param string   $code
602
         * @param string[] $data
603
         *
604
         * @return void
605
         */
606
        protected function addWarning($message, $stackPtr, $code, $data)
607
        {
608
                if (! $this->currentFile) {
8✔
609
                        throw new \Exception('Cannot add warning; current file is not set.');
×
610
                }
611
                $this->currentFile->addWarning(
8✔
612
                        $message,
8✔
613
                        $stackPtr,
8✔
614
                        $code,
8✔
615
                        $data
4✔
616
                );
8✔
617
        }
4✔
618

619
        /**
620
         * Record that a variable has been used within a scope.
621
         *
622
         * If the variable has not been defined first, this will still mark it used.
623
         * To display a warning for undefined variables, use
624
         * `markVariableReadAndWarnIfUndefined()`.
625
         *
626
         * @param string $varName
627
         * @param int    $stackPtr
628
         * @param int    $currScope
629
         *
630
         * @return void
631
         */
632
        protected function markVariableRead($varName, $stackPtr, $currScope)
633
        {
634
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
328✔
635
                if (isset($varInfo->firstRead) && ($varInfo->firstRead <= $stackPtr)) {
328✔
636
                        return;
212✔
637
                }
638
                $varInfo->firstRead = $stackPtr;
328✔
639
        }
164✔
640

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

691
        /**
692
         * Record a variable use and report a warning if the variable is undefined.
693
         *
694
         * @param File   $phpcsFile
695
         * @param string $varName
696
         * @param int    $stackPtr
697
         * @param int    $currScope
698
         *
699
         * @return void
700
         */
701
        protected function markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope)
702
        {
703
                $this->markVariableRead($varName, $stackPtr, $currScope);
328✔
704
                if ($this->isVariableUndefined($varName, $stackPtr, $currScope) === true) {
328✔
705
                        Helpers::debug("variable $varName looks undefined");
248✔
706

707
                        if (Helpers::isVariableArrayPushShortcut($phpcsFile, $stackPtr)) {
248✔
708
                                $this->warnAboutUndefinedArrayPushShortcut($phpcsFile, $varName, $stackPtr);
28✔
709
                                // Mark the variable as defined if it's of the form `$x[] = 1;`
710
                                $this->markVariableAssignment($varName, $stackPtr, $currScope);
28✔
711
                                return;
28✔
712
                        }
713

714
                        if (Helpers::isVariableInsideUnset($phpcsFile, $stackPtr)) {
248✔
715
                                $this->warnAboutUndefinedUnset($phpcsFile, $varName, $stackPtr);
8✔
716
                                return;
8✔
717
                        }
718

719
                        $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
240✔
720
                }
120✔
721
        }
164✔
722

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

748
        /**
749
         * Process a parameter definition if it is inside a function definition.
750
         *
751
         * This does not include variables imported by a "use" statement.
752
         *
753
         * @param File   $phpcsFile
754
         * @param int    $stackPtr
755
         * @param string $varName
756
         * @param int    $outerScope
757
         *
758
         * @return void
759
         */
760
        protected function processVariableAsFunctionParameter(File $phpcsFile, $stackPtr, $varName, $outerScope)
761
        {
762
                Helpers::debug('processVariableAsFunctionParameter', $stackPtr, $varName);
224✔
763
                $tokens = $phpcsFile->getTokens();
224✔
764

765
                $functionPtr = Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
224✔
766
                if (! is_int($functionPtr)) {
224✔
767
                        throw new \Exception("Function index not found for function argument index {$stackPtr}");
×
768
                }
769

770
                Helpers::debug('processVariableAsFunctionParameter found function definition', $tokens[$functionPtr]);
224✔
771
                $this->markVariableDeclaration($varName, ScopeType::PARAM, null, $stackPtr, $functionPtr);
224✔
772

773
                // Are we pass-by-reference?
774
                $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
224✔
775
                if (($referencePtr !== false) && ($tokens[$referencePtr]['code'] === T_BITWISE_AND)) {
224✔
776
                        Helpers::debug('processVariableAsFunctionParameter found pass-by-reference to scope', $outerScope);
28✔
777
                        $varInfo = $this->getOrCreateVariableInfo($varName, $functionPtr);
28✔
778
                        $varInfo->referencedVariableScope = $outerScope;
28✔
779
                }
14✔
780

781
                //  Are we optional with a default?
782
                if (Helpers::getNextAssignPointer($phpcsFile, $stackPtr) !== null) {
224✔
783
                        Helpers::debug('processVariableAsFunctionParameter optional with default');
16✔
784
                        $this->markVariableAssignment($varName, $stackPtr, $functionPtr);
16✔
785
                }
8✔
786

787
                // Are we using constructor promotion? If so, that counts as both definition and use.
788
                if (Helpers::isConstructorPromotion($phpcsFile, $stackPtr)) {
224✔
789
                        Helpers::debug('processVariableAsFunctionParameter constructor promotion');
12✔
790
                        $this->markVariableRead($varName, $stackPtr, $outerScope);
12✔
791
                }
6✔
792
        }
112✔
793

794
        /**
795
         * Process a variable definition if it is inside a function's "use" import.
796
         *
797
         * @param File   $phpcsFile
798
         * @param int    $stackPtr
799
         * @param string $varName
800
         * @param int    $outerScope The start of the scope outside the function definition
801
         *
802
         * @return void
803
         */
804
        protected function processVariableAsUseImportDefinition(File $phpcsFile, $stackPtr, $varName, $outerScope)
805
        {
806
                $tokens = $phpcsFile->getTokens();
24✔
807

808
                Helpers::debug('processVariableAsUseImportDefinition', $stackPtr, $varName, $outerScope);
24✔
809

810
                $endOfArgsPtr = $phpcsFile->findPrevious([T_CLOSE_PARENTHESIS], $stackPtr - 1, null);
24✔
811
                if (! is_int($endOfArgsPtr)) {
24✔
812
                        throw new \Exception("Arguments index not found for function use index {$stackPtr} when processing variable {$varName}");
×
813
                }
814
                $functionPtr = Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $endOfArgsPtr);
24✔
815
                if (! is_int($functionPtr)) {
24✔
816
                        throw new \Exception("Function index not found for function use index {$stackPtr} (using {$endOfArgsPtr}) when processing variable {$varName}");
×
817
                }
818

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

822
                // If it's undefined in the enclosing scope, the use is wrong
823
                if ($this->isVariableUndefined($varName, $stackPtr, $outerScope) === true) {
24✔
824
                        Helpers::debug("variable '{$varName}' in function definition looks undefined in scope", $outerScope);
8✔
825
                        $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
8✔
826
                        return;
8✔
827
                }
828

829
                $this->markVariableDeclaration($varName, ScopeType::BOUND, null, $stackPtr, $functionPtr);
24✔
830
                $this->markVariableAssignment($varName, $stackPtr, $functionPtr);
24✔
831

832
                // Are we pass-by-reference? If so, then any assignment to the variable in
833
                // the function scope also should count for the enclosing scope.
834
                $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
24✔
835
                if (is_int($referencePtr) && $tokens[$referencePtr]['code'] === T_BITWISE_AND) {
24✔
836
                        Helpers::debug("variable '{$varName}' in function definition looks passed by reference");
12✔
837
                        $varInfo = $this->getOrCreateVariableInfo($varName, $functionPtr);
12✔
838
                        $varInfo->referencedVariableScope = $outerScope;
12✔
839
                }
6✔
840
        }
12✔
841

842
        /**
843
         * Process a class property that is being defined.
844
         *
845
         * Property definitions are ignored currently because all property access is
846
         * legal, even to undefined properties.
847
         *
848
         * Can be called for any token and will return false if the variable is not
849
         * of this type.
850
         *
851
         * @param File $phpcsFile
852
         * @param int  $stackPtr
853
         *
854
         * @return bool
855
         */
856
        protected function processVariableAsClassProperty(File $phpcsFile, $stackPtr)
857
        {
858
                $propertyDeclarationKeywords = [
166✔
859
                        T_PUBLIC,
332✔
860
                        T_PRIVATE,
332✔
861
                        T_PROTECTED,
332✔
862
                        T_VAR,
332✔
863
                ];
332✔
864
                $stopAtPtr = $stackPtr - 2;
332✔
865
                $visibilityPtr = $phpcsFile->findPrevious($propertyDeclarationKeywords, $stackPtr - 1, $stopAtPtr > 0 ? $stopAtPtr : 0);
332✔
866
                if ($visibilityPtr) {
332✔
867
                        return true;
4✔
868
                }
869
                $staticPtr = $phpcsFile->findPrevious(T_STATIC, $stackPtr - 1, $stopAtPtr > 0 ? $stopAtPtr : 0);
332✔
870
                if (! $staticPtr) {
332✔
871
                        return false;
332✔
872
                }
873
                $stopAtPtr = $staticPtr - 2;
48✔
874
                $visibilityPtr = $phpcsFile->findPrevious($propertyDeclarationKeywords, $staticPtr - 1, $stopAtPtr > 0 ? $stopAtPtr : 0);
48✔
875
                if ($visibilityPtr) {
48✔
876
                        return true;
4✔
877
                }
878
                // it's legal to use `static` to define properties as well as to
879
                // define variables, so make sure we are not in a function before
880
                // assuming it's a property.
881
                $tokens = $phpcsFile->getTokens();
48✔
882

883
                /** @var array{conditions?: (int|string)[], content?: string}|null */
884
                $token = $tokens[$stackPtr];
48✔
885
                if ($token && !empty($token['conditions']) && !empty($token['content']) && !Helpers::areConditionsWithinFunctionBeforeClass($token)) {
48✔
886
                        return Helpers::areAnyConditionsAClass($token);
4✔
887
                }
888
                return false;
48✔
889
        }
890

891
        /**
892
         * Process a variable that is being accessed inside a catch block.
893
         *
894
         * Can be called for any token and will return false if the variable is not
895
         * of this type.
896
         *
897
         * @param File   $phpcsFile
898
         * @param int    $stackPtr
899
         * @param string $varName
900
         * @param int    $currScope
901
         *
902
         * @return bool
903
         */
904
        protected function processVariableAsCatchBlock(File $phpcsFile, $stackPtr, $varName, $currScope)
905
        {
906
                $tokens = $phpcsFile->getTokens();
340✔
907

908
                // Are we a catch block parameter?
909
                $openPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
340✔
910
                if ($openPtr === null) {
340✔
911
                        return false;
336✔
912
                }
913

914
                $catchPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
256✔
915
                if (($catchPtr !== false) && ($tokens[$catchPtr]['code'] === T_CATCH)) {
256✔
916
                        // Scope of the exception var is actually the function, not just the catch block.
917
                        $this->markVariableDeclaration($varName, ScopeType::LOCAL, null, $stackPtr, $currScope, true);
44✔
918
                        $this->markVariableAssignment($varName, $stackPtr, $currScope);
44✔
919
                        if ($this->allowUnusedCaughtExceptions) {
44✔
920
                                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
40✔
921
                                $varInfo->ignoreUnused = true;
40✔
922
                        }
20✔
923
                        return true;
44✔
924
                }
925
                return false;
212✔
926
        }
927

928
        /**
929
         * Process a variable that is being accessed as a member of `$this`.
930
         *
931
         * Looks for variables of the form `$this->myVariable`.
932
         *
933
         * Can be called for any token and will return false if the variable is not
934
         * of this type.
935
         *
936
         * @param File   $phpcsFile
937
         * @param int    $stackPtr
938
         * @param string $varName
939
         *
940
         * @return bool
941
         */
942
        protected function processVariableAsThisWithinClass(File $phpcsFile, $stackPtr, $varName)
943
        {
944
                $tokens = $phpcsFile->getTokens();
340✔
945
                $token  = $tokens[$stackPtr];
340✔
946

947
                // Are we $this within a class?
948
                if (($varName !== 'this') || empty($token['conditions'])) {
340✔
949
                        return false;
324✔
950
                }
951

952
                // Handle enums specially since their condition may not exist in old phpcs.
953
                $inEnum = false;
72✔
954
                foreach ($this->enums as $enum) {
72✔
955
                        if ($stackPtr > $enum->blockStart && $stackPtr < $enum->blockEnd) {
4✔
956
                                $inEnum = true;
4✔
957
                        }
2✔
958
                }
36✔
959

960
                $inFunction = false;
72✔
961
                foreach (array_reverse($token['conditions'], true) as $scopeCode) {
72✔
962
                        //  $this within a closure is valid
963
                        if ($scopeCode === T_CLOSURE && $inFunction === false) {
72✔
964
                                return true;
12✔
965
                        }
966

967
                        $classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
72✔
968
                        if (defined('T_ENUM')) {
72✔
969
                                $classlikeCodes[] = T_ENUM;
36✔
970
                        }
18✔
971
                        if (in_array($scopeCode, $classlikeCodes, true)) {
72✔
972
                                return true;
64✔
973
                        }
974

975
                        if ($scopeCode === T_FUNCTION && $inEnum) {
72✔
976
                                return true;
4✔
977
                        }
978

979
                        // Handle nested function declarations.
980
                        if ($scopeCode === T_FUNCTION) {
72✔
981
                                if ($inFunction === true) {
72✔
982
                                        break;
4✔
983
                                }
984

985
                                $inFunction = true;
72✔
986
                        }
36✔
987
                }
36✔
988

989
                return false;
12✔
990
        }
991

992
        /**
993
         * Process a superglobal variable that is being accessed.
994
         *
995
         * Can be called for any token and will return false if the variable is not
996
         * of this type.
997
         *
998
         * @param string $varName
999
         *
1000
         * @return bool
1001
         */
1002
        protected function processVariableAsSuperGlobal($varName)
1003
        {
1004
                $superglobals = [
166✔
1005
                        'GLOBALS',
332✔
1006
                        '_SERVER',
332✔
1007
                        '_GET',
332✔
1008
                        '_POST',
332✔
1009
                        '_FILES',
332✔
1010
                        '_COOKIE',
332✔
1011
                        '_SESSION',
332✔
1012
                        '_REQUEST',
332✔
1013
                        '_ENV',
332✔
1014
                        'argv',
332✔
1015
                        'argc',
332✔
1016
                        'http_response_header',
332✔
1017
                        'HTTP_RAW_POST_DATA',
332✔
1018
                ];
332✔
1019
                // Are we a superglobal variable?
1020
                return (in_array($varName, $superglobals, true));
332✔
1021
        }
1022

1023
        /**
1024
         * Process a variable that is being accessed with static syntax.
1025
         *
1026
         * That is, this will record the use of a variable of the form
1027
         * `MyClass::$myVariable` or `self::$myVariable`.
1028
         *
1029
         * Can be called for any token and will return false if the variable is not
1030
         * of this type.
1031
         *
1032
         * @param File $phpcsFile
1033
         * @param int  $stackPtr
1034
         *
1035
         * @return bool
1036
         */
1037
        protected function processVariableAsStaticMember(File $phpcsFile, $stackPtr)
1038
        {
1039
                $tokens = $phpcsFile->getTokens();
332✔
1040

1041
                $doubleColonPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
332✔
1042
                if ($doubleColonPtr === false || $tokens[$doubleColonPtr]['code'] !== T_DOUBLE_COLON) {
332✔
1043
                        return false;
332✔
1044
                }
1045
                $classNamePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $doubleColonPtr - 1, null, true);
28✔
1046
                $staticReferences = [
14✔
1047
                        T_STRING,
28✔
1048
                        T_SELF,
28✔
1049
                        T_PARENT,
28✔
1050
                        T_STATIC,
28✔
1051
                        T_VARIABLE,
28✔
1052
                ];
28✔
1053
                if ($classNamePtr === false || ! in_array($tokens[$classNamePtr]['code'], $staticReferences, true)) {
28✔
1054
                        return false;
×
1055
                }
1056
                // "When calling static methods, the function call is stronger than the
1057
                // static property operator" so look for a function call.
1058
                $parenPointer = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
28✔
1059
                if ($parenPointer !== false && $tokens[$parenPointer]['code'] === T_OPEN_PARENTHESIS) {
28✔
1060
                        return false;
4✔
1061
                }
1062
                return true;
28✔
1063
        }
1064

1065
        /**
1066
         * @param File   $phpcsFile
1067
         * @param int    $stackPtr
1068
         * @param string $varName
1069
         *
1070
         * @return bool
1071
         */
1072
        protected function processVariableAsStaticOutsideClass(File $phpcsFile, $stackPtr, $varName)
1073
        {
1074
                // Are we refering to self:: outside a class?
1075

1076
                $tokens = $phpcsFile->getTokens();
332✔
1077

1078
                /** @var array{conditions?: (int|string)[], content?: string}|null */
1079
                $token = $tokens[$stackPtr];
332✔
1080

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

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

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

1171
                Helpers::debug('processVariableAsAssignment: marking as assignment in scope', $currScope);
312✔
1172
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
312✔
1173

1174
                // If the left-hand-side of the assignment (the variable we are examining)
1175
                // is itself a reference, then that counts as a read as well as a write.
1176
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
312✔
1177
                if ($varInfo->isDynamicReference) {
312✔
1178
                        Helpers::debug('processVariableAsAssignment: also marking as a use because variable is a reference');
16✔
1179
                        $this->markVariableRead($varName, $stackPtr, $currScope);
16✔
1180
                }
8✔
1181
        }
156✔
1182

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

1211
                // OK, we're a [ ... ] construct... are we being assigned to?
1212
                $assignments = Helpers::getListAssignments($phpcsFile, $openPtr);
52✔
1213
                if (! $assignments) {
52✔
1214
                        return false;
48✔
1215
                }
1216
                $matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) {
16✔
1217
                        if ($assignment === $stackPtr) {
32✔
1218
                                return $assignment;
32✔
1219
                        }
1220
                        return $thisAssignment;
24✔
1221
                });
32✔
1222
                if (! $matchingAssignment) {
32✔
1223
                        return false;
20✔
1224
                }
1225

1226
                // Yes, we're being assigned.
1227
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
32✔
1228
                return true;
32✔
1229
        }
1230

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

1255
                // OK, are we within a list (...) construct?
1256
                $openPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
324✔
1257
                if ($openPtr === null) {
324✔
1258
                        return false;
276✔
1259
                }
1260

1261
                $prevPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
212✔
1262
                if ((is_bool($prevPtr)) || ($tokens[$prevPtr]['code'] !== T_LIST)) {
212✔
1263
                        return false;
208✔
1264
                }
1265

1266
                // OK, we're a list (...) construct... are we being assigned to?
1267
                $assignments = Helpers::getListAssignments($phpcsFile, $prevPtr);
32✔
1268
                if (! $assignments) {
32✔
1269
                        return false;
8✔
1270
                }
1271
                $matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) {
16✔
1272
                        if ($assignment === $stackPtr) {
32✔
1273
                                return $assignment;
32✔
1274
                        }
1275
                        return $thisAssignment;
24✔
1276
                });
32✔
1277
                if (! $matchingAssignment) {
32✔
1278
                        return false;
×
1279
                }
1280

1281
                // Yes, we're being assigned.
1282
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
32✔
1283
                return true;
32✔
1284
        }
1285

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

1303
                // Are we a global declaration?
1304
                // Search backwards for first token that isn't whitespace/comment, comma or variable.
1305
                $ignore             = Tokens::$emptyTokens;
324✔
1306
                $ignore[T_VARIABLE] = T_VARIABLE;
324✔
1307
                $ignore[T_COMMA]    = T_COMMA;
324✔
1308

1309
                $globalPtr = $phpcsFile->findPrevious($ignore, $stackPtr - 1, null, true, null, true);
324✔
1310
                if (($globalPtr === false) || ($tokens[$globalPtr]['code'] !== T_GLOBAL)) {
324✔
1311
                        return false;
324✔
1312
                }
1313

1314
                // It's a global declaration.
1315
                $this->markVariableDeclaration($varName, ScopeType::GLOBALSCOPE, null, $stackPtr, $currScope);
36✔
1316
                return true;
36✔
1317
        }
1318

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

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

1376
                // Is the 'static' keyword an anonymous static function declaration? If so,
1377
                // this is not a static variable declaration.
1378
                $tokenAfterStatic = $phpcsFile->findNext(Tokens::$emptyTokens, $staticPtr + 1, null, true, null, true);
52✔
1379
                $functionTokenTypes = [
26✔
1380
                        T_FUNCTION,
52✔
1381
                        T_CLOSURE,
52✔
1382
                        T_FN,
52✔
1383
                ];
52✔
1384
                if (is_int($tokenAfterStatic) && in_array($tokens[$tokenAfterStatic]['code'], $functionTokenTypes, true)) {
52✔
1385
                        return false;
8✔
1386
                }
1387

1388
                // Is the token inside function parameters? If so, this is not a static
1389
                // declaration because we must be inside a function body.
1390
                if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
52✔
UNCOV
1391
                        return false;
×
1392
                }
1393

1394
                // Is the token inside a function call? If so, this is not a static
1395
                // declaration.
1396
                if (Helpers::isTokenInsideFunctionCallArgument($phpcsFile, $stackPtr)) {
52✔
1397
                        return false;
12✔
1398
                }
1399

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

1409
                $this->markVariableDeclaration($varName, ScopeType::STATICSCOPE, null, $stackPtr, $currScope);
36✔
1410
                if (Helpers::getNextAssignPointer($phpcsFile, $stackPtr) !== null) {
36✔
1411
                        $this->markVariableAssignment($varName, $stackPtr, $currScope);
×
1412
                }
1413
                return true;
36✔
1414
        }
1415

1416
        /**
1417
         * @param File   $phpcsFile
1418
         * @param int    $stackPtr
1419
         * @param string $varName
1420
         * @param int    $currScope
1421
         *
1422
         * @return bool
1423
         */
1424
        protected function processVariableAsForeachLoopVar(File $phpcsFile, $stackPtr, $varName, $currScope)
1425
        {
1426
                $tokens = $phpcsFile->getTokens();
320✔
1427

1428
                // Are we a foreach loopvar?
1429
                $openParenPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
320✔
1430
                if (! is_int($openParenPtr)) {
320✔
1431
                        return false;
272✔
1432
                }
1433
                $foreachPtr = Helpers::getIntOrNull($phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true));
208✔
1434
                if (! is_int($foreachPtr)) {
208✔
1435
                        return false;
×
1436
                }
1437
                if ($tokens[$foreachPtr]['code'] === T_LIST) {
208✔
1438
                        $openParenPtr = Helpers::findContainingOpeningBracket($phpcsFile, $foreachPtr);
8✔
1439
                        if (! is_int($openParenPtr)) {
8✔
1440
                                return false;
×
1441
                        }
1442
                        $foreachPtr = Helpers::getIntOrNull($phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true));
8✔
1443
                        if (! is_int($foreachPtr)) {
8✔
1444
                                return false;
×
1445
                        }
1446
                }
4✔
1447
                if ($tokens[$foreachPtr]['code'] !== T_FOREACH) {
208✔
1448
                        return false;
180✔
1449
                }
1450

1451
                // Is there an 'as' token between us and the foreach?
1452
                if ($phpcsFile->findPrevious(T_AS, $stackPtr - 1, $openParenPtr) === false) {
60✔
1453
                        return false;
60✔
1454
                }
1455
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
60✔
1456
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
60✔
1457

1458
                // Is this the value of a key => value foreach?
1459
                if ($phpcsFile->findPrevious(T_DOUBLE_ARROW, $stackPtr - 1, $openParenPtr) !== false) {
60✔
1460
                        $varInfo->isForeachLoopAssociativeValue = true;
20✔
1461
                }
10✔
1462

1463
                // Are we pass-by-reference?
1464
                $referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
60✔
1465
                if (($referencePtr !== false) && ($tokens[$referencePtr]['code'] === T_BITWISE_AND)) {
60✔
1466
                        Helpers::debug('processVariableAsForeachLoopVar: found foreach loop variable assigned by reference');
24✔
1467
                        $varInfo->isDynamicReference = true;
24✔
1468
                }
12✔
1469

1470
                return true;
60✔
1471
        }
1472

1473
        /**
1474
         * @param File   $phpcsFile
1475
         * @param int    $stackPtr
1476
         * @param string $varName
1477
         * @param int    $currScope
1478
         *
1479
         * @return bool
1480
         */
1481
        protected function processVariableAsPassByReferenceFunctionCall(File $phpcsFile, $stackPtr, $varName, $currScope)
1482
        {
1483
                $tokens = $phpcsFile->getTokens();
320✔
1484

1485
                // Are we pass-by-reference to known pass-by-reference function?
1486
                $functionPtr = Helpers::findFunctionCall($phpcsFile, $stackPtr);
320✔
1487
                if ($functionPtr === null || ! isset($tokens[$functionPtr])) {
320✔
1488
                        return false;
306✔
1489
                }
1490

1491
                // Is our function a known pass-by-reference function?
1492
                $functionName = $tokens[$functionPtr]['content'];
154✔
1493
                $refArgs = $this->getPassByReferenceFunction($functionName);
154✔
1494
                if (! $refArgs) {
154✔
1495
                        return false;
150✔
1496
                }
1497

1498
                $argPtrs = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
20✔
1499

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

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

1534
                // Just us, we can mark it as a write.
1535
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
16✔
1536
                // It's a read as well for purposes of used-variables.
1537
                $this->markVariableRead($varName, $stackPtr, $currScope);
16✔
1538
                return true;
16✔
1539
        }
1540

1541
        /**
1542
         * @param File   $phpcsFile
1543
         * @param int    $stackPtr
1544
         * @param string $varName
1545
         * @param int    $currScope
1546
         *
1547
         * @return bool
1548
         */
1549
        protected function processVariableAsSymbolicObjectProperty(File $phpcsFile, $stackPtr, $varName, $currScope)
1550
        {
1551
                $tokens = $phpcsFile->getTokens();
340✔
1552

1553
                // Are we a symbolic object property/function derefeference?
1554
                // Search backwards for first token that isn't whitespace, is it a "->" operator?
1555
                $objectOperatorPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
340✔
1556
                if (($objectOperatorPtr === false) || ($tokens[$objectOperatorPtr]['code'] !== T_OBJECT_OPERATOR)) {
340✔
1557
                        return false;
340✔
1558
                }
1559

1560
                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
28✔
1561
                return true;
28✔
1562
        }
1563

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

1600
                // Get the name of the variable.
1601
                $varName = Helpers::normalizeVarName($token['content']);
344✔
1602
                Helpers::debug("examining token for variable '{$varName}' on line {$token['line']}", $token);
344✔
1603

1604
                // Find the start of the current scope.
1605
                $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr);
344✔
1606
                if ($currScope === null) {
344✔
1607
                        Helpers::debug('no scope found');
52✔
1608
                        return;
52✔
1609
                }
1610
                Helpers::debug("start of scope for variable '{$varName}' is", $currScope);
340✔
1611

1612
                // Determine if variable is being assigned ("write") or used ("read").
1613

1614
                // Read methods that preempt assignment:
1615
                //   Are we a $object->$property type symbolic reference?
1616

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

1632
                // Are we inside the third expression of a for loop? Store such variables
1633
                // for processing after the loop ends by `processClosingForLoopsAt()`.
1634
                if (empty($options['ignore-for-loops'])) {
340✔
1635
                        $forLoop = Helpers::getForLoopForIncrementVariable($stackPtr, $this->forLoops);
340✔
1636
                        if ($forLoop) {
340✔
1637
                                Helpers::debug('found variable inside for loop third expression');
8✔
1638
                                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
8✔
1639
                                $forLoop->incrementVariables[$stackPtr] = $varInfo;
8✔
1640
                                return;
8✔
1641
                        }
1642
                }
170✔
1643

1644
                // Are we a $object->$property type symbolic reference?
1645
                if ($this->processVariableAsSymbolicObjectProperty($phpcsFile, $stackPtr, $varName, $currScope)) {
340✔
1646
                        Helpers::debug('found symbolic object property');
28✔
1647
                        return;
28✔
1648
                }
1649

1650
                // Are we a function or closure parameter?
1651
                if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
340✔
1652
                        Helpers::debug('found function definition parameter');
224✔
1653
                        $this->processVariableAsFunctionParameter($phpcsFile, $stackPtr, $varName, $currScope);
224✔
1654
                        return;
224✔
1655
                }
1656

1657
                // Are we a variable being imported into a function's scope with "use"?
1658
                if (Helpers::isTokenInsideFunctionUseImport($phpcsFile, $stackPtr)) {
340✔
1659
                        Helpers::debug('found use scope import definition');
24✔
1660
                        $this->processVariableAsUseImportDefinition($phpcsFile, $stackPtr, $varName, $currScope);
24✔
1661
                        return;
24✔
1662
                }
1663

1664
                // Are we a catch parameter?
1665
                if ($this->processVariableAsCatchBlock($phpcsFile, $stackPtr, $varName, $currScope)) {
340✔
1666
                        Helpers::debug('found catch block');
44✔
1667
                        return;
44✔
1668
                }
1669

1670
                // Are we $this within a class?
1671
                if ($this->processVariableAsThisWithinClass($phpcsFile, $stackPtr, $varName)) {
340✔
1672
                        Helpers::debug('found this usage within a class');
64✔
1673
                        return;
64✔
1674
                }
1675

1676
                // Are we a $GLOBALS, $_REQUEST, etc superglobal?
1677
                if ($this->processVariableAsSuperGlobal($varName)) {
332✔
1678
                        Helpers::debug('found superglobal');
12✔
1679
                        return;
12✔
1680
                }
1681

1682
                // Check for static members used outside a class
1683
                if ($this->processVariableAsStaticOutsideClass($phpcsFile, $stackPtr, $varName)) {
332✔
1684
                        Helpers::debug('found static usage outside of class');
8✔
1685
                        return;
8✔
1686
                }
1687

1688
                // $var part of class::$var static member
1689
                if ($this->processVariableAsStaticMember($phpcsFile, $stackPtr)) {
332✔
1690
                        Helpers::debug('found static member');
28✔
1691
                        return;
28✔
1692
                }
1693

1694
                if ($this->processVariableAsClassProperty($phpcsFile, $stackPtr)) {
332✔
1695
                        Helpers::debug('found class property definition');
4✔
1696
                        return;
4✔
1697
                }
1698

1699
                // Is the next non-whitespace an assignment?
1700
                if (Helpers::isTokenInsideAssignmentLHS($phpcsFile, $stackPtr)) {
332✔
1701
                        Helpers::debug('found assignment');
312✔
1702
                        $this->processVariableAsAssignment($phpcsFile, $stackPtr, $varName, $currScope);
312✔
1703
                        if (Helpers::isTokenInsideAssignmentRHS($phpcsFile, $stackPtr) || Helpers::isTokenInsideFunctionCallArgument($phpcsFile, $stackPtr)) {
312✔
1704
                                Helpers::debug("found assignment that's also inside an expression");
12✔
1705
                                $this->markVariableRead($varName, $stackPtr, $currScope);
12✔
1706
                                return;
12✔
1707
                        }
1708
                        return;
312✔
1709
                }
1710

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

1717
                // OK, are we within a [...] = construct?
1718
                if ($this->processVariableAsListShorthandAssignment($phpcsFile, $stackPtr, $varName, $currScope)) {
324✔
1719
                        Helpers::debug('found list shorthand assignment');
32✔
1720
                        return;
32✔
1721
                }
1722

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

1729
                // Are we a static declaration?
1730
                if ($this->processVariableAsStaticDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) {
324✔
1731
                        Helpers::debug('found static declaration');
36✔
1732
                        return;
36✔
1733
                }
1734

1735
                // Are we a foreach loopvar?
1736
                if ($this->processVariableAsForeachLoopVar($phpcsFile, $stackPtr, $varName, $currScope)) {
320✔
1737
                        Helpers::debug('found foreach loop variable');
60✔
1738
                        return;
60✔
1739
                }
1740

1741
                // Are we pass-by-reference to known pass-by-reference function?
1742
                if ($this->processVariableAsPassByReferenceFunctionCall($phpcsFile, $stackPtr, $varName, $currScope)) {
320✔
1743
                        Helpers::debug('found pass by reference');
16✔
1744
                        return;
16✔
1745
                }
1746

1747
                // Are we a numeric variable used for constructs like preg_replace?
1748
                if (Helpers::isVariableANumericVariable($varName)) {
320✔
1749
                        Helpers::debug('found numeric variable');
×
1750
                        return;
×
1751
                }
1752

1753
                if (Helpers::isVariableInsideElseCondition($phpcsFile, $stackPtr) || Helpers::isVariableInsideElseBody($phpcsFile, $stackPtr)) {
320✔
1754
                        Helpers::debug('found variable inside else condition or body');
16✔
1755
                        $this->processVaribleInsideElse($phpcsFile, $stackPtr, $varName, $currScope);
16✔
1756
                        return;
16✔
1757
                }
1758

1759
                // Are we an isset or empty call?
1760
                if (Helpers::isVariableInsideIssetOrEmpty($phpcsFile, $stackPtr)) {
320✔
1761
                        Helpers::debug('found isset or empty');
4✔
1762
                        $this->markVariableRead($varName, $stackPtr, $currScope);
4✔
1763
                        return;
4✔
1764
                }
1765

1766
                // OK, we don't appear to be a write to the var, assume we're a read.
1767
                Helpers::debug('looks like a variable read');
320✔
1768
                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
320✔
1769
        }
160✔
1770

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

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

1814
                if (count($assignmentsInsideAttachedBlocks) === count($allAssignmentIndices)) {
16✔
1815
                        if (! $varInfo->ignoreUndefined) {
16✔
1816
                                Helpers::debug("variable $varName inside else looks undefined");
16✔
1817
                                $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
16✔
1818
                        }
8✔
1819
                        return;
16✔
1820
                }
1821

1822
                Helpers::debug('looks like a variable read inside else');
16✔
1823
                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
16✔
1824
        }
8✔
1825

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

1842
                if (!preg_match_all(Constants::getDoubleQuotedVarRegexp(), $token['content'], $matches)) {
164✔
1843
                        return;
24✔
1844
                }
1845
                Helpers::debug('examining token for variable in string', $token);
148✔
1846

1847
                foreach ($matches[1] as $varName) {
148✔
1848
                        $varName = Helpers::normalizeVarName($varName);
148✔
1849

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

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

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

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

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

1873
        /**
1874
         * @param File                   $phpcsFile
1875
         * @param int                    $stackPtr
1876
         * @param array<int, array<int>> $arguments The stack pointers of each argument
1877
         * @param int                    $currScope
1878
         *
1879
         * @return void
1880
         */
1881
        protected function processCompactArguments(File $phpcsFile, $stackPtr, $arguments, $currScope)
1882
        {
1883
                $tokens = $phpcsFile->getTokens();
12✔
1884

1885
                foreach ($arguments as $argumentPtrs) {
12✔
1886
                        $argumentPtrs = array_values(array_filter($argumentPtrs, function ($argumentPtr) use ($tokens) {
6✔
1887
                                return isset(Tokens::$emptyTokens[$tokens[$argumentPtr]['code']]) === false;
12✔
1888
                        }));
12✔
1889
                        if (empty($argumentPtrs)) {
12✔
1890
                                continue;
×
1891
                        }
1892
                        if (!isset($tokens[$argumentPtrs[0]])) {
12✔
1893
                                continue;
×
1894
                        }
1895
                        $argumentFirstToken = $tokens[$argumentPtrs[0]];
12✔
1896
                        if ($argumentFirstToken['code'] === T_ARRAY) {
12✔
1897
                                // It's an array argument, recurse.
1898
                                $arrayArguments = Helpers::findFunctionCallArguments($phpcsFile, $argumentPtrs[0]);
12✔
1899
                                $this->processCompactArguments($phpcsFile, $stackPtr, $arrayArguments, $currScope);
12✔
1900
                                continue;
12✔
1901
                        }
1902
                        if (count($argumentPtrs) > 1) {
12✔
1903
                                // Complex argument, we can't handle it, ignore.
1904
                                continue;
12✔
1905
                        }
1906
                        if ($argumentFirstToken['code'] === T_CONSTANT_ENCAPSED_STRING) {
12✔
1907
                                // Single-quoted string literal, ie compact('whatever').
1908
                                // Substr is to strip the enclosing single-quotes.
1909
                                $varName = substr($argumentFirstToken['content'], 1, -1);
12✔
1910
                                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $argumentPtrs[0], $currScope);
12✔
1911
                                continue;
12✔
1912
                        }
1913
                        if ($argumentFirstToken['code'] === T_DOUBLE_QUOTED_STRING) {
12✔
1914
                                // Double-quoted string literal.
1915
                                if (preg_match(Constants::getDoubleQuotedVarRegexp(), $argumentFirstToken['content'])) {
12✔
1916
                                        // Bail if the string needs variable expansion, that's runtime stuff.
1917
                                        continue;
12✔
1918
                                }
1919
                                // Substr is to strip the enclosing double-quotes.
1920
                                $varName = substr($argumentFirstToken['content'], 1, -1);
×
1921
                                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $argumentPtrs[0], $currScope);
×
1922
                                continue;
×
1923
                        }
1924
                }
6✔
1925
        }
6✔
1926

1927
        /**
1928
         * Called to process variables named in a call to compact().
1929
         *
1930
         * @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
1931
         * @param int  $stackPtr  The position where the call to compact() was found.
1932
         *
1933
         * @return void
1934
         */
1935
        protected function processCompact(File $phpcsFile, $stackPtr)
1936
        {
1937
                $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr);
12✔
1938
                if ($currScope === null) {
12✔
1939
                        return;
×
1940
                }
1941

1942
                $arguments = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
12✔
1943
                $this->processCompactArguments($phpcsFile, $stackPtr, $arguments, $currScope);
12✔
1944
        }
6✔
1945

1946
        /**
1947
         * Called to process the end of a scope.
1948
         *
1949
         * Note that although triggered by the closing curly brace of the scope,
1950
         * $stackPtr is the scope conditional, not the closing curly brace.
1951
         *
1952
         * @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
1953
         * @param int  $stackPtr  The position of the scope conditional.
1954
         *
1955
         * @return void
1956
         */
1957
        protected function processScopeClose(File $phpcsFile, $stackPtr)
1958
        {
1959
                $scopeInfo = $this->scopeManager->getScopeForScopeStart($phpcsFile->getFilename(), $stackPtr);
344✔
1960
                if (is_null($scopeInfo)) {
344✔
1961
                        return;
×
1962
                }
1963
                foreach ($scopeInfo->variables as $varInfo) {
344✔
1964
                        $this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
332✔
1965
                }
172✔
1966
        }
172✔
1967

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

2034
                $this->warnAboutUnusedVariable($phpcsFile, $varInfo);
216✔
2035
        }
108✔
2036

2037
        /**
2038
         * Register warnings for a variable that is defined but not used.
2039
         *
2040
         * @param File         $phpcsFile
2041
         * @param VariableInfo $varInfo
2042
         *
2043
         * @return void
2044
         */
2045
        protected function warnAboutUnusedVariable(File $phpcsFile, VariableInfo $varInfo)
2046
        {
2047
                foreach (array_unique($varInfo->allAssignments) as $indexForWarning) {
216✔
2048
                        Helpers::debug("variable {$varInfo->name} at end of scope looks unused");
216✔
2049
                        $phpcsFile->addWarning(
216✔
2050
                                'Unused %s %s.',
216✔
2051
                                $indexForWarning,
216✔
2052
                                'UnusedVariable',
216✔
2053
                                [
108✔
2054
                                        VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType ?: ScopeType::LOCAL],
216✔
2055
                                        "\${$varInfo->name}",
216✔
2056
                                ]
108✔
2057
                        );
216✔
2058
                }
108✔
2059
        }
108✔
2060

2061
        /**
2062
         * @param File   $phpcsFile
2063
         * @param string $varName
2064
         * @param int    $stackPtr
2065
         *
2066
         * @return void
2067
         */
2068
        protected function warnAboutUndefinedVariable(File $phpcsFile, $varName, $stackPtr)
2069
        {
2070
                $phpcsFile->addWarning(
240✔
2071
                        'Variable %s is undefined.',
240✔
2072
                        $stackPtr,
240✔
2073
                        'UndefinedVariable',
240✔
2074
                        ["\${$varName}"]
240✔
2075
                );
240✔
2076
        }
120✔
2077

2078
        /**
2079
         * @param File   $phpcsFile
2080
         * @param string $varName
2081
         * @param int    $stackPtr
2082
         *
2083
         * @return void
2084
         */
2085
        protected function warnAboutUndefinedArrayPushShortcut(File $phpcsFile, $varName, $stackPtr)
2086
        {
2087
                $phpcsFile->addWarning(
28✔
2088
                        'Array variable %s is undefined.',
28✔
2089
                        $stackPtr,
28✔
2090
                        'UndefinedVariable',
28✔
2091
                        ["\${$varName}"]
28✔
2092
                );
28✔
2093
        }
14✔
2094

2095
        /**
2096
         * @param File   $phpcsFile
2097
         * @param string $varName
2098
         * @param int    $stackPtr
2099
         *
2100
         * @return void
2101
         */
2102
        protected function warnAboutUndefinedUnset(File $phpcsFile, $varName, $stackPtr)
2103
        {
2104
                $phpcsFile->addWarning(
8✔
2105
                        'Variable %s inside unset call is undefined.',
8✔
2106
                        $stackPtr,
8✔
2107
                        'UndefinedUnsetVariable',
8✔
2108
                        ["\${$varName}"]
8✔
2109
                );
8✔
2110
        }
4✔
2111
}
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