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

sirbrillig / phpcs-variable-analysis / 18144438392

30 Sep 2025 09:51PM UTC coverage: 93.382% (-0.4%) from 93.79%
18144438392

Pull #356

github

sirbrillig
Remove phpunit 12
Pull Request #356: Update for phpcs 4.0.0

41 of 52 new or added lines in 2 files covered. (78.85%)

80 existing lines in 1 file now uncovered.

1933 of 2070 relevant lines covered (93.38%)

132.36 hits per line

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

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

3
namespace VariableAnalysis\Sniffs\CodeAnalysis;
4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

252
                $token = $tokens[$stackPtr];
356✔
253

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

552
                // Is the variable referencing another variable? If so, mark that variable used also.
553
                if ($varInfo->referencedVariableScope !== null && $varInfo->referencedVariableScope !== $currScope) {
328✔
554
                        Helpers::debug('markVariableAssignmentWithoutInitialization: considering marking referenced variable assigned', $varName);
24✔
555
                        // 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
556
                        if ($this->getVariableInfo($varInfo->name, $varInfo->referencedVariableScope)) {
24✔
557
                                Helpers::debug('markVariableAssignmentWithoutInitialization: marking referenced variable as assigned also', $varName);
4✔
558
                                $this->markVariableAssignment($varInfo->name, $stackPtr, $varInfo->referencedVariableScope);
4✔
559
                        } else {
1✔
560
                                Helpers::debug('markVariableAssignmentWithoutInitialization: not marking referenced variable assigned', $varName);
22✔
561
                        }
562
                } else {
6✔
563
                                Helpers::debug('markVariableAssignmentWithoutInitialization: not considering referenced variable', $varName);
328✔
564
                }
565

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

837
                Helpers::debug('processVariableAsUseImportDefinition', $stackPtr, $varName, $outerScope);
24✔
838

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

992
                                $inFunction = true;
80✔
993
                        }
20✔
994
                }
20✔
995

996
                return false;
12✔
997
        }
998

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

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

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

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

1083
                $tokens = $phpcsFile->getTokens();
344✔
1084

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1483
                return true;
64✔
1484
        }
1485

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

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

1504
                // Is our function a known pass-by-reference function?
1505
                $functionName = $tokens[$functionPtr]['content'];
162✔
1506

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

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

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

1546
                $argPtrs = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
24✔
1547

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

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

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

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

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

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

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

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

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

1660
                // Determine if variable is being assigned ("write") or used ("read").
1661

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1923
                        $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
140✔
1924
                }
37✔
1925
        }
74✔
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)
12✔
1936
        {
1937
                Helpers::debug("processCompact at {$stackPtr}");
12✔
1938
                $arguments = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
12✔
1939
                $variables = Helpers::getVariablesInsideCompact($phpcsFile, $stackPtr, $arguments);
12✔
1940
                foreach ($variables as $variable) {
12✔
1941
                        $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr, $variable->name);
12✔
1942
                        if ($currScope === null) {
12✔
1943
                                continue;
×
1944
                        }
1945
                        $variablePosition = $variable->firstRead ? $variable->firstRead : $stackPtr;
12✔
1946
                        $this->markVariableReadAndWarnIfUndefined($phpcsFile, $variable->name, $variablePosition, $currScope);
12✔
1947
                }
3✔
1948
        }
6✔
1949

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

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

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

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

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

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

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