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

sirbrillig / phpcs-variable-analysis / 8412362125

24 Mar 2024 09:44PM UTC coverage: 93.851% (-0.1%) from 93.95%
8412362125

push

github

web-flow
Make sure that recursive search of list assignments stays inside them (#318)

* Add tests for destructuring assignment with array arg

* Make sure that recursive search of list assignments stays inside them

19 of 21 new or added lines in 1 file covered. (90.48%)

1 existing line in 1 file now uncovered.

1862 of 1984 relevant lines covered (93.85%)

133.97 hits per line

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

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

3
namespace VariableAnalysis\Sniffs\CodeAnalysis;
4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

156
        public function __construct()
344✔
157
        {
158
                $this->scopeManager = new ScopeManager();
344✔
159
        }
172✔
160

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

542
                if (empty($varInfo->scopeType)) {
316✔
543
                        $varInfo->scopeType = ScopeType::LOCAL;
308✔
544
                }
154✔
545
                $varInfo->allAssignments[] = $stackPtr;
316✔
546
        }
158✔
547

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

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

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

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

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

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

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

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

719
                        if (Helpers::isVariableInsideUnset($phpcsFile, $stackPtr)) {
248✔
720
                                $this->warnAboutUndefinedUnset($phpcsFile, $varName, $stackPtr);
8✔
721
                                return;
8✔
722
                        }
723

724
                        $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
240✔
725
                }
120✔
726
        }
164✔
727

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

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

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

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

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

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

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

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

813
                Helpers::debug('processVariableAsUseImportDefinition', $stackPtr, $varName, $outerScope);
24✔
814

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

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

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

834
                $this->markVariableDeclaration($varName, ScopeType::BOUND, null, $stackPtr, $functionPtr);
24✔
835
                $this->markVariableAssignment($varName, $stackPtr, $functionPtr);
24✔
836

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

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

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

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

913
                // Are we a catch block parameter?
914
                $openPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
340✔
915
                if ($openPtr === null) {
340✔
916
                        return false;
336✔
917
                }
918

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

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

952
                // Are we $this within a class?
953
                if (($varName !== 'this') || empty($token['conditions'])) {
340✔
954
                        return false;
324✔
955
                }
956

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

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

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

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

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

990
                                $inFunction = true;
80✔
991
                        }
40✔
992
                }
40✔
993

994
                return false;
12✔
995
        }
996

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

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

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

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

1081
                $tokens = $phpcsFile->getTokens();
332✔
1082

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1481
                return true;
60✔
1482
        }
1483

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

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

1502
                // Is our function a known pass-by-reference function?
1503
                $functionName = $tokens[$functionPtr]['content'];
158✔
1504
                $refArgs = $this->getPassByReferenceFunction($functionName);
158✔
1505
                if (! $refArgs) {
158✔
1506
                        return false;
154✔
1507
                }
1508

1509
                $argPtrs = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
20✔
1510

1511
                // We're within a function call arguments list, find which arg we are.
1512
                $argPos = false;
20✔
1513
                foreach ($argPtrs as $idx => $ptrs) {
20✔
1514
                        if (in_array($stackPtr, $ptrs)) {
20✔
1515
                                $argPos = $idx + 1;
20✔
1516
                                break;
20✔
1517
                        }
1518
                }
10✔
1519
                if ($argPos === false) {
20✔
1520
                        return false;
×
1521
                }
1522
                if (!in_array($argPos, $refArgs)) {
20✔
1523
                        // Our arg wasn't mentioned explicitly, are we after an elipsis catch-all?
1524
                        $elipsis = array_search('...', $refArgs);
20✔
1525
                        if ($elipsis === false) {
20✔
1526
                                return false;
20✔
1527
                        }
1528
                        $elipsis = (int)$elipsis;
16✔
1529
                        if ($argPos < $refArgs[$elipsis - 1]) {
16✔
1530
                                return false;
16✔
1531
                        }
1532
                }
8✔
1533

1534
                // Our argument position matches that of a pass-by-ref argument,
1535
                // check that we're the only part of the argument expression.
1536
                foreach ($argPtrs[$argPos - 1] as $ptr) {
16✔
1537
                        if ($ptr === $stackPtr) {
16✔
1538
                                continue;
16✔
1539
                        }
1540
                        if (isset(Tokens::$emptyTokens[$tokens[$ptr]['code']]) === false) {
16✔
1541
                                return false;
×
1542
                        }
1543
                }
8✔
1544

1545
                // Just us, we can mark it as a write.
1546
                $this->markVariableAssignment($varName, $stackPtr, $currScope);
16✔
1547
                // It's a read as well for purposes of used-variables.
1548
                $this->markVariableRead($varName, $stackPtr, $currScope);
16✔
1549
                return true;
16✔
1550
        }
1551

1552
        /**
1553
         * @param File   $phpcsFile
1554
         * @param int    $stackPtr
1555
         * @param string $varName
1556
         * @param int    $currScope
1557
         *
1558
         * @return bool
1559
         */
1560
        protected function processVariableAsSymbolicObjectProperty(File $phpcsFile, $stackPtr, $varName, $currScope)
340✔
1561
        {
1562
                $tokens = $phpcsFile->getTokens();
340✔
1563

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

1571
                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
28✔
1572
                return true;
28✔
1573
        }
1574

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

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

1615
                // Find the start of the current scope.
1616
                $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr);
344✔
1617
                if ($currScope === null) {
344✔
1618
                        Helpers::debug('no scope found');
52✔
1619
                        return;
52✔
1620
                }
1621
                Helpers::debug("start of scope for variable '{$varName}' is", $currScope);
340✔
1622

1623
                // Determine if variable is being assigned ("write") or used ("read").
1624

1625
                // Read methods that preempt assignment:
1626
                //   Are we a $object->$property type symbolic reference?
1627

1628
                // Possible assignment methods:
1629
                //   Is a mandatory function/closure parameter
1630
                //   Is an optional function/closure parameter with non-null value
1631
                //   Is closure use declaration of a variable defined within containing scope
1632
                //   catch (...) block start
1633
                //   $this within a class.
1634
                //   $GLOBALS, $_REQUEST, etc superglobals.
1635
                //   $var part of class::$var static member
1636
                //   Assignment via =
1637
                //   Assignment via list (...) =
1638
                //   Declares as a global
1639
                //   Declares as a static
1640
                //   Assignment via foreach (... as ...) { }
1641
                //   Pass-by-reference to known pass-by-reference function
1642

1643
                // Are we inside the third expression of a for loop? Store such variables
1644
                // for processing after the loop ends by `processClosingForLoopsAt()`.
1645
                if (empty($options['ignore-for-loops'])) {
340✔
1646
                        $forLoop = Helpers::getForLoopForIncrementVariable($stackPtr, $this->forLoops);
340✔
1647
                        if ($forLoop) {
340✔
1648
                                Helpers::debug('found variable inside for loop third expression');
8✔
1649
                                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
8✔
1650
                                $forLoop->incrementVariables[$stackPtr] = $varInfo;
8✔
1651
                                return;
8✔
1652
                        }
1653
                }
170✔
1654

1655
                // Are we a $object->$property type symbolic reference?
1656
                if ($this->processVariableAsSymbolicObjectProperty($phpcsFile, $stackPtr, $varName, $currScope)) {
340✔
1657
                        Helpers::debug('found symbolic object property');
28✔
1658
                        return;
28✔
1659
                }
1660

1661
                // Are we a function or closure parameter?
1662
                if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
340✔
1663
                        Helpers::debug('found function definition parameter');
228✔
1664
                        $this->processVariableAsFunctionParameter($phpcsFile, $stackPtr, $varName, $currScope);
228✔
1665
                        return;
228✔
1666
                }
1667

1668
                // Are we a variable being imported into a function's scope with "use"?
1669
                if (Helpers::isTokenInsideFunctionUseImport($phpcsFile, $stackPtr)) {
340✔
1670
                        Helpers::debug('found use scope import definition');
24✔
1671
                        $this->processVariableAsUseImportDefinition($phpcsFile, $stackPtr, $varName, $currScope);
24✔
1672
                        return;
24✔
1673
                }
1674

1675
                // Are we a catch parameter?
1676
                if ($this->processVariableAsCatchBlock($phpcsFile, $stackPtr, $varName, $currScope)) {
340✔
1677
                        Helpers::debug('found catch block');
44✔
1678
                        return;
44✔
1679
                }
1680

1681
                // Are we $this within a class?
1682
                if ($this->processVariableAsThisWithinClass($phpcsFile, $stackPtr, $varName)) {
340✔
1683
                        Helpers::debug('found this usage within a class');
72✔
1684
                        return;
72✔
1685
                }
1686

1687
                // Are we a $GLOBALS, $_REQUEST, etc superglobal?
1688
                if ($this->processVariableAsSuperGlobal($varName)) {
332✔
1689
                        Helpers::debug('found superglobal');
12✔
1690
                        return;
12✔
1691
                }
1692

1693
                // Check for static members used outside a class
1694
                if ($this->processVariableAsStaticOutsideClass($phpcsFile, $stackPtr, $varName)) {
332✔
1695
                        Helpers::debug('found static usage outside of class');
8✔
1696
                        return;
8✔
1697
                }
1698

1699
                // $var part of class::$var static member
1700
                if ($this->processVariableAsStaticMember($phpcsFile, $stackPtr)) {
332✔
1701
                        Helpers::debug('found static member');
28✔
1702
                        return;
28✔
1703
                }
1704

1705
                if ($this->processVariableAsClassProperty($phpcsFile, $stackPtr)) {
332✔
1706
                        Helpers::debug('found class property definition');
4✔
1707
                        return;
4✔
1708
                }
1709

1710
                // Is the next non-whitespace an assignment?
1711
                if (Helpers::isTokenInsideAssignmentLHS($phpcsFile, $stackPtr)) {
332✔
1712
                        Helpers::debug('found assignment');
312✔
1713
                        $this->processVariableAsAssignment($phpcsFile, $stackPtr, $varName, $currScope);
312✔
1714
                        if (Helpers::isTokenInsideAssignmentRHS($phpcsFile, $stackPtr) || Helpers::isTokenInsideFunctionCallArgument($phpcsFile, $stackPtr)) {
312✔
1715
                                Helpers::debug("found assignment that's also inside an expression");
12✔
1716
                                $this->markVariableRead($varName, $stackPtr, $currScope);
12✔
1717
                                return;
12✔
1718
                        }
1719
                        return;
312✔
1720
                }
1721

1722
                // OK, are we within a list (...) = construct?
1723
                if ($this->processVariableAsListAssignment($phpcsFile, $stackPtr, $varName, $currScope)) {
324✔
1724
                        Helpers::debug('found list assignment');
32✔
1725
                        return;
32✔
1726
                }
1727

1728
                // OK, are we within a [...] = construct?
1729
                if ($this->processVariableAsListShorthandAssignment($phpcsFile, $stackPtr, $varName, $currScope)) {
324✔
1730
                        Helpers::debug('found list shorthand assignment');
32✔
1731
                        return;
32✔
1732
                }
1733

1734
                // Are we a global declaration?
1735
                if ($this->processVariableAsGlobalDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) {
324✔
1736
                        Helpers::debug('found global declaration');
36✔
1737
                        return;
36✔
1738
                }
1739

1740
                // Are we a static declaration?
1741
                if ($this->processVariableAsStaticDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) {
324✔
1742
                        Helpers::debug('found static declaration');
36✔
1743
                        return;
36✔
1744
                }
1745

1746
                // Are we a foreach loopvar?
1747
                if ($this->processVariableAsForeachLoopVar($phpcsFile, $stackPtr, $varName, $currScope)) {
320✔
1748
                        Helpers::debug('found foreach loop variable');
60✔
1749
                        return;
60✔
1750
                }
1751

1752
                // Are we pass-by-reference to known pass-by-reference function?
1753
                if ($this->processVariableAsPassByReferenceFunctionCall($phpcsFile, $stackPtr, $varName, $currScope)) {
320✔
1754
                        Helpers::debug('found pass by reference');
16✔
1755
                        return;
16✔
1756
                }
1757

1758
                // Are we a numeric variable used for constructs like preg_replace?
1759
                if (Helpers::isVariableANumericVariable($varName)) {
320✔
1760
                        Helpers::debug('found numeric variable');
×
1761
                        return;
×
1762
                }
1763

1764
                if (Helpers::isVariableInsideElseCondition($phpcsFile, $stackPtr) || Helpers::isVariableInsideElseBody($phpcsFile, $stackPtr)) {
320✔
1765
                        Helpers::debug('found variable inside else condition or body');
24✔
1766
                        $this->processVaribleInsideElse($phpcsFile, $stackPtr, $varName, $currScope);
24✔
1767
                        return;
24✔
1768
                }
1769

1770
                // Are we an isset or empty call?
1771
                if (Helpers::isVariableInsideIssetOrEmpty($phpcsFile, $stackPtr)) {
320✔
1772
                        Helpers::debug('found isset or empty');
20✔
1773
                        $this->markVariableRead($varName, $stackPtr, $currScope);
20✔
1774
                        return;
20✔
1775
                }
1776

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

1782
        /**
1783
         * @param File   $phpcsFile
1784
         * @param int    $stackPtr
1785
         * @param string $varName
1786
         * @param int    $currScope
1787
         *
1788
         * @return void
1789
         */
1790
        protected function processVaribleInsideElse(File $phpcsFile, $stackPtr, $varName, $currScope)
24✔
1791
        {
1792
                // Find all assignments to this variable inside the current scope.
1793
                $varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
24✔
1794
                $allAssignmentIndices = array_unique($varInfo->allAssignments);
24✔
1795
                // Find the attached 'if' and 'elseif' block start and end indices.
1796
                $blockIndices = Helpers::getAttachedBlockIndicesForElse($phpcsFile, $stackPtr);
24✔
1797

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

1825
                if (count($assignmentsInsideAttachedBlocks) === count($allAssignmentIndices)) {
24✔
1826
                        if (! $varInfo->ignoreUndefined) {
16✔
1827
                                Helpers::debug("variable $varName inside else looks undefined");
16✔
1828
                                $this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
16✔
1829
                        }
8✔
1830
                        return;
16✔
1831
                }
1832

1833
                Helpers::debug('looks like a variable read inside else');
24✔
1834
                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
24✔
1835
        }
12✔
1836

1837
        /**
1838
         * Called to process variables found in double quoted strings.
1839
         *
1840
         * Note that there may be more than one variable in the string, which will
1841
         * result only in one call for the string.
1842
         *
1843
         * @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
1844
         * @param int  $stackPtr  The position where the double quoted string was found.
1845
         *
1846
         * @return void
1847
         */
1848
        protected function processVariableInString(File $phpcsFile, $stackPtr)
164✔
1849
        {
1850
                $tokens = $phpcsFile->getTokens();
164✔
1851
                $token  = $tokens[$stackPtr];
164✔
1852

1853
                $regexp = Constants::getDoubleQuotedVarRegexp();
164✔
1854
                if (! empty($regexp) && !preg_match_all($regexp, $token['content'], $matches)) {
164✔
1855
                        Helpers::debug('processVariableInString: no variables found', $token);
24✔
1856
                        return;
24✔
1857
                }
1858
                Helpers::debug('examining token for variable in string', $token);
148✔
1859

1860
                if (empty($matches)) {
148✔
1861
                        Helpers::debug('processVariableInString: no variables found after search', $token);
×
1862
                        return;
×
1863
                }
1864
                foreach ($matches[1] as $varName) {
148✔
1865
                        $varName = Helpers::normalizeVarName($varName);
148✔
1866

1867
                        // Are we $this within a class?
1868
                        if ($this->processVariableAsThisWithinClass($phpcsFile, $stackPtr, $varName)) {
148✔
1869
                                continue;
8✔
1870
                        }
1871

1872
                        if ($this->processVariableAsSuperGlobal($varName)) {
140✔
1873
                                continue;
12✔
1874
                        }
1875

1876
                        // Are we a numeric variable used for constructs like preg_replace?
1877
                        if (Helpers::isVariableANumericVariable($varName)) {
140✔
1878
                                continue;
4✔
1879
                        }
1880

1881
                        $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr, $varName);
140✔
1882
                        if ($currScope === null) {
140✔
1883
                                continue;
×
1884
                        }
1885

1886
                        $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
140✔
1887
                }
74✔
1888
        }
74✔
1889

1890
        /**
1891
         * @param File                   $phpcsFile
1892
         * @param int                    $stackPtr
1893
         * @param array<int, array<int>> $arguments The stack pointers of each argument
1894
         * @param int                    $currScope
1895
         *
1896
         * @return void
1897
         */
1898
        protected function processCompactArguments(File $phpcsFile, $stackPtr, $arguments, $currScope)
12✔
1899
        {
1900
                $tokens = $phpcsFile->getTokens();
12✔
1901

1902
                foreach ($arguments as $argumentPtrs) {
12✔
1903
                        $argumentPtrs = array_values(array_filter($argumentPtrs, function ($argumentPtr) use ($tokens) {
12✔
1904
                                return isset(Tokens::$emptyTokens[$tokens[$argumentPtr]['code']]) === false;
12✔
1905
                        }));
12✔
1906
                        if (empty($argumentPtrs)) {
12✔
1907
                                continue;
×
1908
                        }
1909
                        if (!isset($tokens[$argumentPtrs[0]])) {
12✔
1910
                                continue;
×
1911
                        }
1912
                        $argumentFirstToken = $tokens[$argumentPtrs[0]];
12✔
1913
                        if ($argumentFirstToken['code'] === T_ARRAY) {
12✔
1914
                                // It's an array argument, recurse.
1915
                                $arrayArguments = Helpers::findFunctionCallArguments($phpcsFile, $argumentPtrs[0]);
12✔
1916
                                $this->processCompactArguments($phpcsFile, $stackPtr, $arrayArguments, $currScope);
12✔
1917
                                continue;
12✔
1918
                        }
1919
                        if (count($argumentPtrs) > 1) {
12✔
1920
                                // Complex argument, we can't handle it, ignore.
1921
                                continue;
12✔
1922
                        }
1923
                        if ($argumentFirstToken['code'] === T_CONSTANT_ENCAPSED_STRING) {
12✔
1924
                                // Single-quoted string literal, ie compact('whatever').
1925
                                // Substr is to strip the enclosing single-quotes.
1926
                                $varName = substr($argumentFirstToken['content'], 1, -1);
12✔
1927
                                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $argumentPtrs[0], $currScope);
12✔
1928
                                continue;
12✔
1929
                        }
1930
                        if ($argumentFirstToken['code'] === T_DOUBLE_QUOTED_STRING) {
12✔
1931
                                // Double-quoted string literal.
1932
                                $regexp = Constants::getDoubleQuotedVarRegexp();
12✔
1933
                                if (! empty($regexp) && preg_match($regexp, $argumentFirstToken['content'])) {
12✔
1934
                                        // Bail if the string needs variable expansion, that's runtime stuff.
1935
                                        continue;
12✔
1936
                                }
1937
                                // Substr is to strip the enclosing double-quotes.
1938
                                $varName = substr($argumentFirstToken['content'], 1, -1);
×
1939
                                $this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $argumentPtrs[0], $currScope);
×
1940
                                continue;
×
1941
                        }
1942
                }
6✔
1943
        }
6✔
1944

1945
        /**
1946
         * Called to process variables named in a call to compact().
1947
         *
1948
         * @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
1949
         * @param int  $stackPtr  The position where the call to compact() was found.
1950
         *
1951
         * @return void
1952
         */
1953
        protected function processCompact(File $phpcsFile, $stackPtr)
12✔
1954
        {
1955
                $currScope = Helpers::findVariableScope($phpcsFile, $stackPtr);
12✔
1956
                if ($currScope === null) {
12✔
1957
                        return;
×
1958
                }
1959

1960
                $arguments = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
12✔
1961
                $this->processCompactArguments($phpcsFile, $stackPtr, $arguments, $currScope);
12✔
1962
        }
6✔
1963

1964
        /**
1965
         * Called to process the end of a scope.
1966
         *
1967
         * Note that although triggered by the closing curly brace of the scope,
1968
         * $stackPtr is the scope conditional, not the closing curly brace.
1969
         *
1970
         * @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
1971
         * @param int  $stackPtr  The position of the scope conditional.
1972
         *
1973
         * @return void
1974
         */
1975
        protected function processScopeClose(File $phpcsFile, $stackPtr)
344✔
1976
        {
1977
                Helpers::debug("processScopeClose at {$stackPtr}");
344✔
1978
                $scopeInfo = $this->scopeManager->getScopeForScopeStart($phpcsFile->getFilename(), $stackPtr);
344✔
1979
                if (is_null($scopeInfo)) {
344✔
1980
                        return;
×
1981
                }
1982
                foreach ($scopeInfo->variables as $varInfo) {
344✔
1983
                        $this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
332✔
1984
                }
172✔
1985
        }
172✔
1986

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

2055
                $this->warnAboutUnusedVariable($phpcsFile, $varInfo);
216✔
2056
        }
108✔
2057

2058
        /**
2059
         * Register warnings for a variable that is defined but not used.
2060
         *
2061
         * @param File         $phpcsFile
2062
         * @param VariableInfo $varInfo
2063
         *
2064
         * @return void
2065
         */
2066
        protected function warnAboutUnusedVariable(File $phpcsFile, VariableInfo $varInfo)
216✔
2067
        {
2068
                foreach (array_unique($varInfo->allAssignments) as $indexForWarning) {
216✔
2069
                        Helpers::debug("variable '{$varInfo->name}' at end of scope looks unused");
216✔
2070
                        $phpcsFile->addWarning(
216✔
2071
                                'Unused %s %s.',
216✔
2072
                                $indexForWarning,
216✔
2073
                                'UnusedVariable',
216✔
2074
                                [
108✔
2075
                                        VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType ?: ScopeType::LOCAL],
216✔
2076
                                        "\${$varInfo->name}",
216✔
2077
                                ]
108✔
2078
                        );
216✔
2079
                }
108✔
2080
        }
108✔
2081

2082
        /**
2083
         * @param File   $phpcsFile
2084
         * @param string $varName
2085
         * @param int    $stackPtr
2086
         *
2087
         * @return void
2088
         */
2089
        protected function warnAboutUndefinedVariable(File $phpcsFile, $varName, $stackPtr)
240✔
2090
        {
2091
                $phpcsFile->addWarning(
240✔
2092
                        'Variable %s is undefined.',
240✔
2093
                        $stackPtr,
240✔
2094
                        'UndefinedVariable',
240✔
2095
                        ["\${$varName}"]
240✔
2096
                );
240✔
2097
        }
120✔
2098

2099
        /**
2100
         * @param File   $phpcsFile
2101
         * @param string $varName
2102
         * @param int    $stackPtr
2103
         *
2104
         * @return void
2105
         */
2106
        protected function warnAboutUndefinedArrayPushShortcut(File $phpcsFile, $varName, $stackPtr)
28✔
2107
        {
2108
                $phpcsFile->addWarning(
28✔
2109
                        'Array variable %s is undefined.',
28✔
2110
                        $stackPtr,
28✔
2111
                        'UndefinedVariable',
28✔
2112
                        ["\${$varName}"]
28✔
2113
                );
28✔
2114
        }
14✔
2115

2116
        /**
2117
         * @param File   $phpcsFile
2118
         * @param string $varName
2119
         * @param int    $stackPtr
2120
         *
2121
         * @return void
2122
         */
2123
        protected function warnAboutUndefinedUnset(File $phpcsFile, $varName, $stackPtr)
8✔
2124
        {
2125
                $phpcsFile->addWarning(
8✔
2126
                        'Variable %s inside unset call is undefined.',
8✔
2127
                        $stackPtr,
8✔
2128
                        'UndefinedUnsetVariable',
8✔
2129
                        ["\${$varName}"]
8✔
2130
                );
8✔
2131
        }
4✔
2132
}
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

© 2025 Coveralls, Inc