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

sirbrillig / phpcs-variable-analysis / 24632078366

19 Apr 2026 02:39PM UTC coverage: 94.489% (+0.8%) from 93.732%
24632078366

Pull #360

github

sirbrillig
Simplify getUseIndexForUseImport using findContainingOpeningBracket()

Replaces the manual findPrevious call with an exclusion list with the
existing findContainingOpeningBracket() abstraction, and simplifies the
second findPrevious to skip only empty tokens.
Pull Request #360: Migrate to PHPCSUtils

67 of 70 new or added lines in 2 files covered. (95.71%)

8 existing lines in 2 files now uncovered.

1749 of 1851 relevant lines covered (94.49%)

138.7 hits per line

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

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

3
namespace VariableAnalysis\Sniffs\CodeAnalysis;
4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

998
                return false;
12✔
999
        }
1000

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1485
                return true;
64✔
1486
        }
1487

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2105
        /**
2106
         * @param File   $phpcsFile
2107
         * @param string $varName
2108
         * @param int    $stackPtr
2109
         *
2110
         * @return void
2111
         */
2112
        protected function warnAboutUndefinedUnset(File $phpcsFile, $varName, $stackPtr)
8✔
2113
        {
2114
                $phpcsFile->addWarning(
8✔
2115
                        'Variable %s inside unset call is undefined.',
8✔
2116
                        $stackPtr,
8✔
2117
                        'UndefinedUnsetVariable',
8✔
2118
                        ["\${$varName}"]
8✔
2119
                );
6✔
2120
        }
4✔
2121
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc