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

PHPCSStandards / PHP_CodeSniffer / 17690595468

13 Sep 2025 02:38AM UTC coverage: 78.779% (-0.007%) from 78.786%
17690595468

push

github

jrfnl
Merge branch 'master' into 4.x

4 of 6 new or added lines in 5 files covered. (66.67%)

2 existing lines in 2 files now uncovered.

19724 of 25037 relevant lines covered (78.78%)

96.5 hits per line

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

69.41
/src/Files/File.php
1
<?php
2
/**
3
 * Represents a piece of content being checked during the run.
4
 *
5
 * @author    Greg Sherwood <gsherwood@squiz.net>
6
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9

10
namespace PHP_CodeSniffer\Files;
11

12
use PHP_CodeSniffer\Config;
13
use PHP_CodeSniffer\Exceptions\RuntimeException;
14
use PHP_CodeSniffer\Exceptions\TokenizerException;
15
use PHP_CodeSniffer\Fixer;
16
use PHP_CodeSniffer\Ruleset;
17
use PHP_CodeSniffer\Tokenizers\PHP;
18
use PHP_CodeSniffer\Util\Common;
19
use PHP_CodeSniffer\Util\Tokens;
20
use PHP_CodeSniffer\Util\Writers\StatusWriter;
21

22
class File
23
{
24

25
    /**
26
     * The absolute path to the file associated with this object.
27
     *
28
     * @var string
29
     */
30
    public $path = '';
31

32
    /**
33
     * The content of the file.
34
     *
35
     * @var string
36
     */
37
    protected $content = '';
38

39
    /**
40
     * The config data for the run.
41
     *
42
     * @var \PHP_CodeSniffer\Config
43
     */
44
    public $config = null;
45

46
    /**
47
     * The ruleset used for the run.
48
     *
49
     * @var \PHP_CodeSniffer\Ruleset
50
     */
51
    public $ruleset = null;
52

53
    /**
54
     * If TRUE, the entire file is being ignored.
55
     *
56
     * @var boolean
57
     */
58
    public $ignored = false;
59

60
    /**
61
     * The EOL character this file uses.
62
     *
63
     * @var string
64
     */
65
    public $eolChar = '';
66

67
    /**
68
     * The Fixer object to control fixing errors.
69
     *
70
     * @var \PHP_CodeSniffer\Fixer
71
     */
72
    public $fixer = null;
73

74
    /**
75
     * The tokenizer being used for this file.
76
     *
77
     * @var \PHP_CodeSniffer\Tokenizers\PHP
78
     */
79
    public $tokenizer = null;
80

81
    /**
82
     * Was the file loaded from cache?
83
     *
84
     * If TRUE, the file was loaded from a local cache.
85
     * If FALSE, the file was tokenized and processed fully.
86
     *
87
     * @var boolean
88
     */
89
    public $fromCache = false;
90

91
    /**
92
     * The number of tokens in this file.
93
     *
94
     * Stored here to save calling count() everywhere.
95
     *
96
     * @var integer
97
     */
98
    public $numTokens = 0;
99

100
    /**
101
     * The tokens stack map.
102
     *
103
     * @var array
104
     */
105
    protected $tokens = [];
106

107
    /**
108
     * The errors raised from sniffs.
109
     *
110
     * @var array
111
     * @see getErrors()
112
     */
113
    protected $errors = [];
114

115
    /**
116
     * The warnings raised from sniffs.
117
     *
118
     * @var array
119
     * @see getWarnings()
120
     */
121
    protected $warnings = [];
122

123
    /**
124
     * The metrics recorded by sniffs.
125
     *
126
     * @var array
127
     * @see getMetrics()
128
     */
129
    protected $metrics = [];
130

131
    /**
132
     * The metrics recorded for each token.
133
     *
134
     * Stops the same metric being recorded for the same token twice.
135
     *
136
     * @var array
137
     * @see getMetrics()
138
     */
139
    private $metricTokens = [];
140

141
    /**
142
     * The total number of errors raised.
143
     *
144
     * @var integer
145
     */
146
    protected $errorCount = 0;
147

148
    /**
149
     * The total number of warnings raised.
150
     *
151
     * @var integer
152
     */
153
    protected $warningCount = 0;
154

155
    /**
156
     * The original total number of errors and warnings (first run on a file).
157
     *
158
     * {@internal This should be regarded as an immutable property.}
159
     *
160
     * @var array<string, int>
161
     */
162
    private $firstRunCounts = [];
163

164
    /**
165
     * The current total number of errors that can be fixed.
166
     *
167
     * @var integer
168
     */
169
    protected $fixableErrorCount = 0;
170

171
    /**
172
     * The current total number of warnings that can be fixed.
173
     *
174
     * @var integer
175
     */
176
    protected $fixableWarningCount = 0;
177

178
    /**
179
     * The actual number of errors and warnings that were fixed.
180
     *
181
     * I.e. how many fixes were applied. This may be higher than the originally found
182
     * issues if the fixer from one sniff causes other sniffs to come into play in follow-up loops.
183
     * Example: if a brace is moved to a new line, the `ScopeIndent` sniff may need to ensure
184
     * the brace is indented correctly in the next loop.
185
     *
186
     * @var integer
187
     */
188
    protected $fixedCount = 0;
189

190
    /**
191
     * The effective number of errors that were fixed.
192
     *
193
     * I.e. how many of the originally found errors were fixed.
194
     *
195
     * @var integer
196
     */
197
    protected $fixedErrorCount = 0;
198

199
    /**
200
     * The effective number of warnings that were fixed.
201
     *
202
     * I.e. how many of the originally found warnings were fixed.
203
     *
204
     * @var integer
205
     */
206
    protected $fixedWarningCount = 0;
207

208
    /**
209
     * TRUE if errors are being replayed from the cache.
210
     *
211
     * @var boolean
212
     */
213
    protected $replayingErrors = false;
214

215
    /**
216
     * An array of sniffs that are being ignored.
217
     *
218
     * @var array
219
     */
220
    protected $ignoredListeners = [];
221

222
    /**
223
     * An array of message codes that are being ignored.
224
     *
225
     * @var array
226
     */
227
    protected $ignoredCodes = [];
228

229
    /**
230
     * An array of sniffs listening to this file's processing.
231
     *
232
     * @var \PHP_CodeSniffer\Sniffs\Sniff[]
233
     */
234
    protected $listeners = [];
235

236
    /**
237
     * The class name of the sniff currently processing the file.
238
     *
239
     * @var string
240
     */
241
    protected $activeListener = '';
242

243
    /**
244
     * An array of sniffs being processed and how long they took.
245
     *
246
     * @var array
247
     * @see getListenerTimes()
248
     */
249
    protected $listenerTimes = [];
250

251
    /**
252
     * A cache of often used config settings to improve performance.
253
     *
254
     * Storing them here saves 10k+ calls to __get() in the Config class.
255
     *
256
     * @var array
257
     */
258
    protected $configCache = [];
259

260

261
    /**
262
     * Constructs a file.
263
     *
264
     * @param string                   $path    The absolute path to the file to process.
265
     * @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run.
266
     * @param \PHP_CodeSniffer\Config  $config  The config data for the run.
267
     *
268
     * @return void
269
     */
270
    public function __construct(string $path, Ruleset $ruleset, Config $config)
×
271
    {
272
        $this->path    = $path;
×
273
        $this->ruleset = $ruleset;
×
274
        $this->config  = $config;
×
275
        $this->fixer   = new Fixer();
×
276

277
        $this->configCache['cache']           = $this->config->cache;
×
278
        $this->configCache['sniffs']          = array_map('strtolower', $this->config->sniffs);
×
279
        $this->configCache['exclude']         = array_map('strtolower', $this->config->exclude);
×
280
        $this->configCache['errorSeverity']   = $this->config->errorSeverity;
×
281
        $this->configCache['warningSeverity'] = $this->config->warningSeverity;
×
282
        $this->configCache['recordErrors']    = $this->config->recordErrors;
×
283
        $this->configCache['trackTime']       = $this->config->trackTime;
×
284
        $this->configCache['ignorePatterns']  = $this->ruleset->ignorePatterns;
×
285
        $this->configCache['includePatterns'] = $this->ruleset->includePatterns;
×
286
    }
287

288

289
    /**
290
     * Set the content of the file.
291
     *
292
     * Setting the content also calculates the EOL char being used.
293
     *
294
     * @param string $content The file content.
295
     *
296
     * @return void
297
     */
298
    public function setContent(string $content)
×
299
    {
300
        $this->content = $content;
×
301
        $this->tokens  = [];
×
302

303
        try {
304
            $this->eolChar = Common::detectLineEndings($content);
×
305
        } catch (RuntimeException $e) {
×
306
            $this->addWarningOnLine($e->getMessage(), 1, 'Internal.DetectLineEndings');
×
307
            return;
×
308
        }
309
    }
310

311

312
    /**
313
     * Reloads the content of the file.
314
     *
315
     * By default, we have no idea where our content comes from,
316
     * so we can't do anything.
317
     *
318
     * @return void
319
     */
320
    public function reloadContent()
×
321
    {
322
    }
×
323

324

325
    /**
326
     * Disables caching of this file.
327
     *
328
     * @return void
329
     */
330
    public function disableCaching()
×
331
    {
332
        $this->configCache['cache'] = false;
×
333
    }
334

335

336
    /**
337
     * Starts the stack traversal and tells listeners when tokens are found.
338
     *
339
     * @return void
340
     */
341
    public function process()
×
342
    {
343
        if ($this->ignored === true) {
×
344
            return;
×
345
        }
346

347
        $this->errors            = [];
×
348
        $this->warnings          = [];
×
349
        $this->errorCount        = 0;
×
350
        $this->warningCount      = 0;
×
351
        $this->fixableErrorCount = 0;
×
352
        $this->fixableWarningCount = 0;
×
353

354
        $this->parse();
×
355

356
        // Check if tokenizer errors cause this file to be ignored.
357
        if ($this->ignored === true) {
×
358
            return;
×
359
        }
360

361
        $this->fixer->startFile($this);
×
362

363
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
364
            StatusWriter::write('*** START TOKEN PROCESSING ***', 1);
×
365
        }
366

367
        $foundCode        = false;
×
368
        $listenerIgnoreTo = [];
×
369
        $inTests          = defined('PHP_CODESNIFFER_IN_TESTS');
×
370
        $checkAnnotations = $this->config->annotations;
×
371

372
        // Foreach of the listeners that have registered to listen for this
373
        // token, get them to process it.
374
        foreach ($this->tokens as $stackPtr => $token) {
×
375
            // Check for ignored lines.
376
            if ($checkAnnotations === true
×
377
                && ($token['code'] === T_COMMENT
×
378
                || $token['code'] === T_PHPCS_IGNORE_FILE
×
379
                || $token['code'] === T_PHPCS_SET
×
380
                || $token['code'] === T_DOC_COMMENT_STRING
×
381
                || $token['code'] === T_DOC_COMMENT_TAG
×
382
                || ($inTests === true && $token['code'] === T_INLINE_HTML))
×
383
            ) {
384
                $commentText      = ltrim($this->tokens[$stackPtr]['content'], " \t/*#");
×
385
                $commentTextLower = strtolower($commentText);
×
386
                if (substr($commentTextLower, 0, 16) === 'phpcs:ignorefile'
×
387
                    || substr($commentTextLower, 0, 17) === '@phpcs:ignorefile'
×
388
                ) {
389
                    // Ignoring the whole file, just a little late.
390
                    $this->errors            = [];
×
391
                    $this->warnings          = [];
×
392
                    $this->errorCount        = 0;
×
393
                    $this->warningCount      = 0;
×
394
                    $this->fixableErrorCount = 0;
×
395
                    $this->fixableWarningCount = 0;
×
396
                    return;
×
397
                } elseif (substr($commentTextLower, 0, 9) === 'phpcs:set'
×
398
                    || substr($commentTextLower, 0, 10) === '@phpcs:set'
×
399
                ) {
400
                    if (isset($token['sniffCode']) === true) {
×
401
                        $listenerCode = $token['sniffCode'];
×
402
                        if (isset($this->ruleset->sniffCodes[$listenerCode]) === true) {
×
403
                            $propertyCode  = $token['sniffProperty'];
×
404
                            $settings      = [
405
                                'value' => $token['sniffPropertyValue'],
×
406
                                'scope' => 'sniff',
×
407
                            ];
408
                            $listenerClass = $this->ruleset->sniffCodes[$listenerCode];
×
409
                            $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $settings);
×
410
                        }
411
                    }
412
                }
413
            }
414

415
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
416
                $type    = $token['type'];
×
417
                $content = Common::prepareForOutput($token['content']);
×
418
                StatusWriter::write("Process token $stackPtr: $type => $content", 2);
×
419
            }
420

421
            if ($token['code'] !== T_INLINE_HTML) {
×
422
                $foundCode = true;
×
423
            }
424

425
            if (isset($this->ruleset->tokenListeners[$token['code']]) === false) {
×
426
                continue;
×
427
            }
428

429
            foreach ($this->ruleset->tokenListeners[$token['code']] as $listenerData) {
×
430
                if (isset($this->ignoredListeners[$listenerData['class']]) === true
×
431
                    || (isset($listenerIgnoreTo[$listenerData['class']]) === true
×
432
                    && $listenerIgnoreTo[$listenerData['class']] > $stackPtr)
×
433
                ) {
434
                    // This sniff is ignoring past this token, or the whole file.
435
                    continue;
×
436
                }
437

438
                $class = $listenerData['class'];
×
439

440
                if (trim($this->path, '\'"') !== 'STDIN') {
×
441
                    // If the file path matches one of our ignore patterns, skip it.
442
                    // While there is support for a type of each pattern
443
                    // (absolute or relative) we don't actually support it here.
444
                    foreach ($listenerData['ignore'] as $pattern) {
×
445
                        // We assume a / directory separator, as do the exclude rules
446
                        // most developers write, so we need a special case for any system
447
                        // that is different.
448
                        if (DIRECTORY_SEPARATOR === '\\') {
×
449
                            $pattern = str_replace('/', '\\\\', $pattern);
×
450
                        }
451

452
                        $pattern = '`' . $pattern . '`i';
×
453
                        if (preg_match($pattern, $this->path) === 1) {
×
454
                            $this->ignoredListeners[$class] = true;
×
455
                            continue(2);
×
456
                        }
457
                    }
458

459
                    // If the file path does not match one of our include patterns, skip it.
460
                    // While there is support for a type of each pattern
461
                    // (absolute or relative) we don't actually support it here.
462
                    if (empty($listenerData['include']) === false) {
×
463
                        $included = false;
×
464
                        foreach ($listenerData['include'] as $pattern) {
×
465
                            // We assume a / directory separator, as do the exclude rules
466
                            // most developers write, so we need a special case for any system
467
                            // that is different.
468
                            if (DIRECTORY_SEPARATOR === '\\') {
×
469
                                $pattern = str_replace('/', '\\\\', $pattern);
×
470
                            }
471

472
                            $pattern = '`' . $pattern . '`i';
×
473
                            if (preg_match($pattern, $this->path) === 1) {
×
474
                                $included = true;
×
475
                                break;
×
476
                            }
477
                        }
478

479
                        if ($included === false) {
×
480
                            $this->ignoredListeners[$class] = true;
×
481
                            continue;
×
482
                        }
483
                    }
484
                }
485

486
                $this->activeListener = $class;
×
487

488
                if ($this->configCache['trackTime'] === true) {
×
489
                    $startTime = microtime(true);
×
490
                }
491

492
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
493
                    StatusWriter::write('Processing ' . $this->activeListener . '... ', 3, 0);
×
494
                }
495

496
                $ignoreTo = $this->ruleset->sniffs[$class]->process($this, $stackPtr);
×
497
                if ($ignoreTo !== null) {
×
498
                    $listenerIgnoreTo[$this->activeListener] = $ignoreTo;
×
499
                }
500

501
                if ($this->configCache['trackTime'] === true) {
×
502
                    $timeTaken = (microtime(true) - $startTime);
×
503
                    if (isset($this->listenerTimes[$this->activeListener]) === false) {
×
504
                        $this->listenerTimes[$this->activeListener] = 0;
×
505
                    }
506

507
                    $this->listenerTimes[$this->activeListener] += $timeTaken;
×
508
                }
509

510
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
511
                    $timeTaken = round(($timeTaken), 4);
×
512
                    StatusWriter::write("DONE in $timeTaken seconds");
×
513
                }
514

515
                $this->activeListener = '';
×
516
            }
517
        }
518

519
        // If short open tags are off but the file being checked uses
520
        // short open tags, the whole content will be inline HTML
521
        // and nothing will be checked. So try and handle this case.
522
        // We don't show this error for STDIN because we can't be sure the content
523
        // actually came directly from the user. It could be something like
524
        // refs from a Git pre-push hook.
525
        if ($foundCode === false && $this->path !== 'STDIN') {
×
526
            $shortTags = (bool) ini_get('short_open_tag');
×
527
            if ($shortTags === false) {
×
528
                $error = 'No PHP code was found in this file and short open tags are not allowed by this install of PHP. This file may be using short open tags but PHP does not allow them.';
×
529
                $this->addWarning($error, null, 'Internal.NoCodeFound');
×
530
            }
531
        }
532

533
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
534
            StatusWriter::write('*** END TOKEN PROCESSING ***', 1);
×
535
            StatusWriter::write('*** START SNIFF PROCESSING REPORT ***', 1);
×
536

537
            arsort($this->listenerTimes, SORT_NUMERIC);
×
538
            foreach ($this->listenerTimes as $listener => $timeTaken) {
×
539
                StatusWriter::write("$listener: " . round(($timeTaken), 4) . ' secs', 1);
×
540
            }
541

542
            StatusWriter::write('*** END SNIFF PROCESSING REPORT ***', 1);
×
543
        }
544

NEW
545
        if (empty($this->firstRunCounts) === true) {
×
546
            $this->firstRunCounts = [
×
547
                'error'          => $this->errorCount,
×
548
                'warning'        => $this->warningCount,
×
549
                'fixableError'   => $this->fixableErrorCount,
×
550
                'fixableWarning' => $this->fixableWarningCount,
×
551
            ];
552
        }
553

554
        $this->fixedCount       += $this->fixer->getFixCount();
×
555
        $this->fixedErrorCount   = ($this->firstRunCounts['fixableError'] - $this->fixableErrorCount);
×
556
        $this->fixedWarningCount = ($this->firstRunCounts['fixableWarning'] - $this->fixableWarningCount);
×
557
    }
558

559

560
    /**
561
     * Tokenizes the file and prepares it for the test run.
562
     *
563
     * @return void
564
     */
565
    public function parse()
×
566
    {
567
        if (empty($this->tokens) === false) {
×
568
            // File has already been parsed.
569
            return;
×
570
        }
571

572
        try {
573
            $this->tokenizer = new PHP($this->content, $this->config, $this->eolChar);
×
574
            $this->tokens    = $this->tokenizer->getTokens();
×
575
        } catch (TokenizerException $e) {
×
576
            $this->ignored = true;
×
577
            $this->addWarning($e->getMessage(), null, 'Internal.Tokenizer.Exception');
×
578
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
579
                $newlines = 0;
×
580
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
581
                    $newlines = 1;
×
582
                }
583

584
                StatusWriter::write('[tokenizer error]... ', 0, $newlines);
×
585
            }
586

587
            return;
×
588
        }
589

590
        $this->numTokens = count($this->tokens);
×
591

592
        // Check for mixed line endings as these can cause tokenizer errors and we
593
        // should let the user know that the results they get may be incorrect.
594
        // This is done by removing all backslashes, removing the newline char we
595
        // detected, then converting newlines chars into text. If any backslashes
596
        // are left at the end, we have additional newline chars in use.
597
        $contents = str_replace('\\', '', $this->content);
×
598
        $contents = str_replace($this->eolChar, '', $contents);
×
599
        $contents = str_replace("\n", '\n', $contents);
×
600
        $contents = str_replace("\r", '\r', $contents);
×
601
        if (strpos($contents, '\\') !== false) {
×
602
            $error = 'File has mixed line endings; this may cause incorrect results';
×
603
            $this->addWarningOnLine($error, 1, 'Internal.LineEndings.Mixed');
×
604
        }
605

606
        if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
607
            if ($this->numTokens === 0) {
×
608
                $numLines = 0;
×
609
            } else {
610
                $numLines = $this->tokens[($this->numTokens - 1)]['line'];
×
611
            }
612

613
            $newlines = 0;
×
614
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
615
                $newlines = 1;
×
616
            }
617

618
            StatusWriter::write("[$this->numTokens tokens in $numLines lines]... ", 0, $newlines);
×
619
        }
620
    }
621

622

623
    /**
624
     * Returns the token stack for this file.
625
     *
626
     * @return array
627
     */
628
    public function getTokens()
×
629
    {
630
        return $this->tokens;
×
631
    }
632

633

634
    /**
635
     * Remove vars stored in this file that are no longer required.
636
     *
637
     * @return void
638
     */
639
    public function cleanUp()
×
640
    {
641
        $this->listenerTimes = null;
×
642
        $this->content       = null;
×
643
        $this->tokens        = null;
×
644
        $this->metricTokens  = null;
×
645
        $this->tokenizer     = null;
×
646
        $this->fixer         = null;
×
647
        $this->config        = null;
×
648
        $this->ruleset       = null;
×
649
    }
650

651

652
    /**
653
     * Records an error against a specific token in the file.
654
     *
655
     * @param string   $error    The error message.
656
     * @param int|null $stackPtr The stack position where the error occurred.
657
     * @param string   $code     A violation code unique to the sniff message.
658
     * @param array    $data     Replacements for the error message.
659
     * @param int      $severity The severity level for this error. A value of 0
660
     *                           will be converted into the default severity level.
661
     * @param boolean  $fixable  Can the error be fixed by the sniff?
662
     *
663
     * @return boolean
664
     */
665
    public function addError(
×
666
        string $error,
667
        ?int $stackPtr,
668
        string $code,
669
        array $data = [],
670
        int $severity = 0,
671
        bool $fixable = false
672
    ) {
673
        if ($stackPtr === null) {
×
674
            $line   = 1;
×
675
            $column = 1;
×
676
        } else {
677
            $line   = $this->tokens[$stackPtr]['line'];
×
678
            $column = $this->tokens[$stackPtr]['column'];
×
679
        }
680

681
        return $this->addMessage(true, $error, $line, $column, $code, $data, $severity, $fixable);
×
682
    }
683

684

685
    /**
686
     * Records a warning against a specific token in the file.
687
     *
688
     * @param string   $warning  The error message.
689
     * @param int|null $stackPtr The stack position where the error occurred.
690
     * @param string   $code     A violation code unique to the sniff message.
691
     * @param array    $data     Replacements for the warning message.
692
     * @param int      $severity The severity level for this warning. A value of 0
693
     *                           will be converted into the default severity level.
694
     * @param boolean  $fixable  Can the warning be fixed by the sniff?
695
     *
696
     * @return boolean
697
     */
698
    public function addWarning(
×
699
        string $warning,
700
        ?int $stackPtr,
701
        string $code,
702
        array $data = [],
703
        int $severity = 0,
704
        bool $fixable = false
705
    ) {
706
        if ($stackPtr === null) {
×
707
            $line   = 1;
×
708
            $column = 1;
×
709
        } else {
710
            $line   = $this->tokens[$stackPtr]['line'];
×
711
            $column = $this->tokens[$stackPtr]['column'];
×
712
        }
713

714
        return $this->addMessage(false, $warning, $line, $column, $code, $data, $severity, $fixable);
×
715
    }
716

717

718
    /**
719
     * Records an error against a specific line in the file.
720
     *
721
     * @param string $error    The error message.
722
     * @param int    $line     The line on which the error occurred.
723
     * @param string $code     A violation code unique to the sniff message.
724
     * @param array  $data     Replacements for the error message.
725
     * @param int    $severity The severity level for this error. A value of 0
726
     *                         will be converted into the default severity level.
727
     *
728
     * @return boolean
729
     */
730
    public function addErrorOnLine(
×
731
        string $error,
732
        int $line,
733
        string $code,
734
        array $data = [],
735
        int $severity = 0
736
    ) {
737
        return $this->addMessage(true, $error, $line, 1, $code, $data, $severity, false);
×
738
    }
739

740

741
    /**
742
     * Records a warning against a specific line in the file.
743
     *
744
     * @param string $warning  The error message.
745
     * @param int    $line     The line on which the warning occurred.
746
     * @param string $code     A violation code unique to the sniff message.
747
     * @param array  $data     Replacements for the warning message.
748
     * @param int    $severity The severity level for this warning. A value of 0 will
749
     *                         will be converted into the default severity level.
750
     *
751
     * @return boolean
752
     */
753
    public function addWarningOnLine(
×
754
        string $warning,
755
        int $line,
756
        string $code,
757
        array $data = [],
758
        int $severity = 0
759
    ) {
760
        return $this->addMessage(false, $warning, $line, 1, $code, $data, $severity, false);
×
761
    }
762

763

764
    /**
765
     * Records a fixable error against a specific token in the file.
766
     *
767
     * Returns true if the error was recorded and should be fixed.
768
     *
769
     * @param string $error    The error message.
770
     * @param int    $stackPtr The stack position where the error occurred.
771
     * @param string $code     A violation code unique to the sniff message.
772
     * @param array  $data     Replacements for the error message.
773
     * @param int    $severity The severity level for this error. A value of 0
774
     *                         will be converted into the default severity level.
775
     *
776
     * @return boolean
777
     */
778
    public function addFixableError(
×
779
        string $error,
780
        int $stackPtr,
781
        string $code,
782
        array $data = [],
783
        int $severity = 0
784
    ) {
785
        $recorded = $this->addError($error, $stackPtr, $code, $data, $severity, true);
×
786
        if ($recorded === true && $this->fixer->enabled === true) {
×
787
            return true;
×
788
        }
789

790
        return false;
×
791
    }
792

793

794
    /**
795
     * Records a fixable warning against a specific token in the file.
796
     *
797
     * Returns true if the warning was recorded and should be fixed.
798
     *
799
     * @param string $warning  The error message.
800
     * @param int    $stackPtr The stack position where the error occurred.
801
     * @param string $code     A violation code unique to the sniff message.
802
     * @param array  $data     Replacements for the warning message.
803
     * @param int    $severity The severity level for this warning. A value of 0
804
     *                         will be converted into the default severity level.
805
     *
806
     * @return boolean
807
     */
808
    public function addFixableWarning(
×
809
        string $warning,
810
        int $stackPtr,
811
        string $code,
812
        array $data = [],
813
        int $severity = 0
814
    ) {
815
        $recorded = $this->addWarning($warning, $stackPtr, $code, $data, $severity, true);
×
816
        if ($recorded === true && $this->fixer->enabled === true) {
×
817
            return true;
×
818
        }
819

820
        return false;
×
821
    }
822

823

824
    /**
825
     * Adds an error to the error stack.
826
     *
827
     * @param boolean $error    Is this an error message?
828
     * @param string  $message  The text of the message.
829
     * @param int     $line     The line on which the message occurred.
830
     * @param int     $column   The column at which the message occurred.
831
     * @param string  $code     A violation code unique to the sniff message.
832
     * @param array   $data     Replacements for the message.
833
     * @param int     $severity The severity level for this message. A value of 0
834
     *                          will be converted into the default severity level.
835
     * @param boolean $fixable  Can the problem be fixed by the sniff?
836
     *
837
     * @return boolean
838
     */
839
    protected function addMessage(
291✔
840
        bool $error,
841
        string $message,
842
        int $line,
843
        int $column,
844
        string $code,
845
        array $data,
846
        int $severity,
847
        bool $fixable
848
    ) {
849
        // Check if this line is ignoring all message codes.
850
        if (isset($this->tokenizer->ignoredLines[$line]) === true && $this->tokenizer->ignoredLines[$line]->ignoresEverything() === true) {
291✔
851
            return false;
132✔
852
        }
853

854
        // Work out which sniff generated the message.
855
        $parts = explode('.', $code);
204✔
856
        if ($parts[0] === 'Internal') {
204✔
857
            // An internal message.
858
            $listenerCode = '';
18✔
859
            if ($this->activeListener !== '') {
18✔
860
                $listenerCode = Common::getSniffCode($this->activeListener);
×
861
            }
862

863
            $sniffCode  = $code;
18✔
864
            $checkCodes = [$sniffCode];
18✔
865
        } else {
866
            if ($parts[0] !== $code) {
186✔
867
                // The full message code has been passed in.
868
                $sniffCode    = $code;
×
869
                $listenerCode = substr($sniffCode, 0, strrpos($sniffCode, '.'));
×
870
            } else {
871
                $listenerCode = Common::getSniffCode($this->activeListener);
186✔
872
                $sniffCode    = $listenerCode . '.' . $code;
186✔
873
                $parts        = explode('.', $sniffCode);
186✔
874
            }
875

876
            $checkCodes = [
124✔
877
                $sniffCode,
186✔
878
                $parts[0] . '.' . $parts[1] . '.' . $parts[2],
186✔
879
                $parts[0] . '.' . $parts[1],
186✔
880
                $parts[0],
186✔
881
            ];
124✔
882
        }
883

884
        if (isset($this->tokenizer->ignoredLines[$line]) === true && $this->tokenizer->ignoredLines[$line]->isIgnored($sniffCode) === true) {
204✔
885
            return false;
108✔
886
        }
887

888
        $includeAll = true;
177✔
889
        if ($this->configCache['cache'] === false
177✔
890
            || $this->configCache['recordErrors'] === false
177✔
891
        ) {
892
            $includeAll = false;
177✔
893
        }
894

895
        // Filter out any messages for sniffs that shouldn't have run
896
        // due to the use of the --sniffs or --exclude command line argument,
897
        // but don't filter out "Internal" messages.
898
        if ($includeAll === false
177✔
899
            && (($parts[0] !== 'Internal'
177✔
900
            && empty($this->configCache['sniffs']) === false
177✔
901
            && in_array(strtolower($listenerCode), $this->configCache['sniffs'], true) === false)
171✔
902
            || (empty($this->configCache['exclude']) === false
177✔
903
            && in_array(strtolower($listenerCode), $this->configCache['exclude'], true) === true))
177✔
904
        ) {
905
            return false;
×
906
        }
907

908
        // If we know this sniff code is being ignored for this file, return early.
909
        foreach ($checkCodes as $checkCode) {
177✔
910
            if (isset($this->ignoredCodes[$checkCode]) === true) {
177✔
911
                return false;
×
912
            }
913
        }
914

915
        $oppositeType = 'warning';
177✔
916
        if ($error === false) {
177✔
917
            $oppositeType = 'error';
75✔
918
        }
919

920
        foreach ($checkCodes as $checkCode) {
177✔
921
            // Make sure this message type has not been set to the opposite message type.
922
            if (isset($this->ruleset->ruleset[$checkCode]['type']) === true
177✔
923
                && $this->ruleset->ruleset[$checkCode]['type'] === $oppositeType
177✔
924
            ) {
925
                $error = !$error;
×
926
                break;
×
927
            }
928
        }
929

930
        if ($error === true) {
177✔
931
            $configSeverity = $this->configCache['errorSeverity'];
153✔
932
            $messageCount   = &$this->errorCount;
153✔
933
            $messages       = &$this->errors;
153✔
934
        } else {
935
            $configSeverity = $this->configCache['warningSeverity'];
75✔
936
            $messageCount   = &$this->warningCount;
75✔
937
            $messages       = &$this->warnings;
75✔
938
        }
939

940
        if ($includeAll === false && $configSeverity === 0) {
177✔
941
            // Don't bother doing any processing as these messages are just going to
942
            // be hidden in the reports anyway.
943
            return false;
×
944
        }
945

946
        if ($severity === 0) {
177✔
947
            $severity = 5;
177✔
948
        }
949

950
        foreach ($checkCodes as $checkCode) {
177✔
951
            // Make sure we are interested in this severity level.
952
            if (isset($this->ruleset->ruleset[$checkCode]['severity']) === true) {
177✔
953
                $severity = $this->ruleset->ruleset[$checkCode]['severity'];
9✔
954
                break;
9✔
955
            }
956
        }
957

958
        if ($includeAll === false && $configSeverity > $severity) {
177✔
959
            return false;
9✔
960
        }
961

962
        // Make sure we are not ignoring this file.
963
        $included = null;
168✔
964
        if (trim($this->path, '\'"') === 'STDIN') {
168✔
965
            $included = true;
168✔
966
        } else {
967
            foreach ($checkCodes as $checkCode) {
×
968
                $patterns = null;
×
969

970
                if (isset($this->configCache['includePatterns'][$checkCode]) === true) {
×
971
                    $patterns  = $this->configCache['includePatterns'][$checkCode];
×
972
                    $excluding = false;
×
973
                } elseif (isset($this->configCache['ignorePatterns'][$checkCode]) === true) {
×
974
                    $patterns  = $this->configCache['ignorePatterns'][$checkCode];
×
975
                    $excluding = true;
×
976
                }
977

978
                if ($patterns === null) {
×
979
                    continue;
×
980
                }
981

982
                foreach ($patterns as $pattern => $type) {
×
983
                    // While there is support for a type of each pattern
984
                    // (absolute or relative) we don't actually support it here.
985
                    $replacements = [
986
                        '\\,' => ',',
×
987
                        '*'   => '.*',
988
                    ];
989

990
                    // We assume a / directory separator, as do the exclude rules
991
                    // most developers write, so we need a special case for any system
992
                    // that is different.
993
                    if (DIRECTORY_SEPARATOR === '\\') {
×
994
                        $replacements['/'] = '\\\\';
×
995
                    }
996

997
                    $pattern = '`' . strtr($pattern, $replacements) . '`i';
×
998
                    $matched = preg_match($pattern, $this->path);
×
999

1000
                    if ($matched === 0) {
×
1001
                        if ($excluding === false && $included === null) {
×
1002
                            // This file path is not being included.
1003
                            $included = false;
×
1004
                        }
1005

1006
                        continue;
×
1007
                    }
1008

1009
                    if ($excluding === true) {
×
1010
                        // This file path is being excluded.
1011
                        $this->ignoredCodes[$checkCode] = true;
×
1012
                        return false;
×
1013
                    }
1014

1015
                    // This file path is being included.
1016
                    $included = true;
×
1017
                    break;
×
1018
                }
1019
            }
1020
        }
1021

1022
        if ($included === false) {
168✔
1023
            // There were include rules set, but this file
1024
            // path didn't match any of them.
1025
            return false;
×
1026
        }
1027

1028
        $messageCount++;
168✔
1029
        if ($fixable === true) {
168✔
1030
            if ($error === true) {
135✔
1031
                $this->fixableErrorCount++;
135✔
1032
            } else {
1033
                $this->fixableWarningCount++;
×
1034
            }
1035
        }
1036

1037
        if ($this->configCache['recordErrors'] === false
168✔
1038
            && $includeAll === false
168✔
1039
        ) {
1040
            return true;
×
1041
        }
1042

1043
        // See if there is a custom error message format to use.
1044
        // But don't do this if we are replaying errors because replayed
1045
        // errors have already used the custom format and have had their
1046
        // data replaced.
1047
        if ($this->replayingErrors === false
168✔
1048
            && isset($this->ruleset->ruleset[$sniffCode]['message']) === true
168✔
1049
        ) {
1050
            $message = $this->ruleset->ruleset[$sniffCode]['message'];
×
1051
        }
1052

1053
        if (empty($data) === false) {
168✔
1054
            $message = vsprintf($message, $data);
156✔
1055
        }
1056

1057
        if (isset($messages[$line]) === false) {
168✔
1058
            $messages[$line] = [];
168✔
1059
        }
1060

1061
        if (isset($messages[$line][$column]) === false) {
168✔
1062
            $messages[$line][$column] = [];
168✔
1063
        }
1064

1065
        $messages[$line][$column][] = [
168✔
1066
            'message'  => $message,
168✔
1067
            'source'   => $sniffCode,
168✔
1068
            'listener' => $this->activeListener,
168✔
1069
            'severity' => $severity,
168✔
1070
            'fixable'  => $fixable,
168✔
1071
        ];
112✔
1072

1073
        if (PHP_CODESNIFFER_VERBOSITY > 1
168✔
1074
            && $this->fixer->enabled === true
168✔
1075
            && $fixable === true
168✔
1076
        ) {
1077
            StatusWriter::forceWrite("E: [Line $line] $message ($sniffCode)", 1);
×
1078
        }
1079

1080
        return true;
168✔
1081
    }
1082

1083

1084
    /**
1085
     * Record a metric about the file being examined.
1086
     *
1087
     * @param int    $stackPtr The stack position where the metric was recorded.
1088
     * @param string $metric   The name of the metric being recorded.
1089
     * @param string $value    The value of the metric being recorded.
1090
     *
1091
     * @return boolean
1092
     */
1093
    public function recordMetric(int $stackPtr, string $metric, string $value)
×
1094
    {
1095
        if (isset($this->metrics[$metric]) === false) {
×
1096
            $this->metrics[$metric] = ['values' => [$value => 1]];
×
1097
            $this->metricTokens[$metric][$stackPtr] = true;
×
1098
        } elseif (isset($this->metricTokens[$metric][$stackPtr]) === false) {
×
1099
            $this->metricTokens[$metric][$stackPtr] = true;
×
1100
            if (isset($this->metrics[$metric]['values'][$value]) === false) {
×
1101
                $this->metrics[$metric]['values'][$value] = 1;
×
1102
            } else {
1103
                $this->metrics[$metric]['values'][$value]++;
×
1104
            }
1105
        }
1106

1107
        return true;
×
1108
    }
1109

1110

1111
    /**
1112
     * Returns the number of errors raised.
1113
     *
1114
     * @return int
1115
     */
1116
    public function getErrorCount()
×
1117
    {
1118
        return $this->errorCount;
×
1119
    }
1120

1121

1122
    /**
1123
     * Returns the number of warnings raised.
1124
     *
1125
     * @return int
1126
     */
1127
    public function getWarningCount()
×
1128
    {
1129
        return $this->warningCount;
×
1130
    }
1131

1132

1133
    /**
1134
     * Returns the number of fixable errors/warnings raised.
1135
     *
1136
     * @return int
1137
     */
1138
    public function getFixableCount()
×
1139
    {
1140
        return ($this->fixableErrorCount + $this->fixableWarningCount);
×
1141
    }
1142

1143

1144
    /**
1145
     * Returns the number of fixable errors raised.
1146
     *
1147
     * @return int
1148
     */
1149
    public function getFixableErrorCount()
×
1150
    {
1151
        return $this->fixableErrorCount;
×
1152
    }
1153

1154

1155
    /**
1156
     * Returns the number of fixable warnings raised.
1157
     *
1158
     * @return int
1159
     */
1160
    public function getFixableWarningCount()
×
1161
    {
1162
        return $this->fixableWarningCount;
×
1163
    }
1164

1165

1166
    /**
1167
     * Returns the actual number of fixed errors/warnings.
1168
     *
1169
     * @return int
1170
     */
1171
    public function getFixedCount()
×
1172
    {
1173
        return $this->fixedCount;
×
1174
    }
1175

1176

1177
    /**
1178
     * Returns the effective number of fixed errors.
1179
     *
1180
     * @return int
1181
     */
1182
    public function getFixedErrorCount()
×
1183
    {
1184
        return $this->fixedErrorCount;
×
1185
    }
1186

1187

1188
    /**
1189
     * Returns the effective number of fixed warnings.
1190
     *
1191
     * @return int
1192
     */
1193
    public function getFixedWarningCount()
×
1194
    {
1195
        return $this->fixedWarningCount;
×
1196
    }
1197

1198

1199
    /**
1200
     * Retrieve information about the first run.
1201
     *
1202
     * @param string $type The type for which to get the "first run" count.
1203
     *                     Valid values are: 'error', 'warning', 'fixableError' and 'fixableWarning'.
1204
     *
1205
     * @internal This method does not form part of any public API nor backwards compatibility guarantee.
1206
     *
1207
     * @return int
1208
     */
1209
    public function getFirstRunCount(string $type): int
×
1210
    {
1211
        return $this->firstRunCounts[$type];
×
1212
    }
1213

1214

1215
    /**
1216
     * Returns the list of ignored lines.
1217
     *
1218
     * @return array
1219
     */
1220
    public function getIgnoredLines()
×
1221
    {
1222
        return $this->tokenizer->ignoredLines;
×
1223
    }
1224

1225

1226
    /**
1227
     * Returns the errors raised from processing this file.
1228
     *
1229
     * @return array
1230
     */
1231
    public function getErrors()
×
1232
    {
1233
        return $this->errors;
×
1234
    }
1235

1236

1237
    /**
1238
     * Returns the warnings raised from processing this file.
1239
     *
1240
     * @return array
1241
     */
1242
    public function getWarnings()
×
1243
    {
1244
        return $this->warnings;
×
1245
    }
1246

1247

1248
    /**
1249
     * Returns the metrics found while processing this file.
1250
     *
1251
     * @return array
1252
     */
1253
    public function getMetrics()
×
1254
    {
1255
        return $this->metrics;
×
1256
    }
1257

1258

1259
    /**
1260
     * Returns the time taken processing this file for each invoked sniff.
1261
     *
1262
     * @return array
1263
     */
1264
    public function getListenerTimes()
×
1265
    {
1266
        return $this->listenerTimes;
×
1267
    }
1268

1269

1270
    /**
1271
     * Returns the absolute filename of this file.
1272
     *
1273
     * @return string
1274
     */
1275
    public function getFilename()
×
1276
    {
1277
        return $this->path;
×
1278
    }
1279

1280

1281
    /**
1282
     * Returns the declaration name for classes, interfaces, traits, enums, and functions.
1283
     *
1284
     * @param int $stackPtr The position of the declaration token which
1285
     *                      declared the class, interface, trait, or function.
1286
     *
1287
     * @return string The name of the class, interface, trait, or function or an empty string
1288
     *                if the name could not be determined (live coding).
1289
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type
1290
     *                                                      T_FUNCTION, T_CLASS, T_TRAIT, T_ENUM, or T_INTERFACE.
1291
     */
1292
    public function getDeclarationName(int $stackPtr)
87✔
1293
    {
1294
        $tokenCode = $this->tokens[$stackPtr]['code'];
87✔
1295

1296
        if ($tokenCode !== T_FUNCTION
87✔
1297
            && $tokenCode !== T_CLASS
87✔
1298
            && $tokenCode !== T_INTERFACE
87✔
1299
            && $tokenCode !== T_TRAIT
87✔
1300
            && $tokenCode !== T_ENUM
87✔
1301
        ) {
1302
            throw new RuntimeException('Token type "' . $this->tokens[$stackPtr]['type'] . '" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM');
18✔
1303
        }
1304

1305
        $stopPoint = $this->numTokens;
69✔
1306
        if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === true) {
69✔
1307
            // For functions, stop searching at the parenthesis opener.
1308
            $stopPoint = $this->tokens[$stackPtr]['parenthesis_opener'];
36✔
1309
        } elseif (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
33✔
1310
            // For OO tokens, stop searching at the open curly.
1311
            $stopPoint = $this->tokens[$stackPtr]['scope_opener'];
30✔
1312
        }
1313

1314
        $content = '';
69✔
1315
        for ($i = $stackPtr; $i < $stopPoint; $i++) {
69✔
1316
            if ($this->tokens[$i]['code'] === T_STRING) {
69✔
1317
                $content = $this->tokens[$i]['content'];
63✔
1318
                break;
63✔
1319
            }
1320
        }
1321

1322
        return $content;
69✔
1323
    }
1324

1325

1326
    /**
1327
     * Returns the method parameters for the specified function token.
1328
     *
1329
     * Also supports passing in a USE token for a closure use group.
1330
     *
1331
     * Each parameter is in the following format:
1332
     *
1333
     * <code>
1334
     *   0 => array(
1335
     *         'name'                => string,        // The variable name.
1336
     *         'token'               => integer,       // The stack pointer to the variable name.
1337
     *         'content'             => string,        // The full content of the variable definition.
1338
     *         'has_attributes'      => boolean,       // Does the parameter have one or more attributes attached ?
1339
     *         'pass_by_reference'   => boolean,       // Is the variable passed by reference?
1340
     *         'reference_token'     => integer|false, // The stack pointer to the reference operator
1341
     *                                                 // or FALSE if the param is not passed by reference.
1342
     *         'variable_length'     => boolean,       // Is the param of variable length through use of `...` ?
1343
     *         'variadic_token'      => integer|false, // The stack pointer to the ... operator
1344
     *                                                 // or FALSE if the param is not variable length.
1345
     *         'type_hint'           => string,        // The type hint for the variable.
1346
     *         'type_hint_token'     => integer|false, // The stack pointer to the start of the type hint
1347
     *                                                 // or FALSE if there is no type hint.
1348
     *         'type_hint_end_token' => integer|false, // The stack pointer to the end of the type hint
1349
     *                                                 // or FALSE if there is no type hint.
1350
     *         'nullable_type'       => boolean,       // TRUE if the type is preceded by the nullability
1351
     *                                                 // operator.
1352
     *         'comma_token'         => integer|false, // The stack pointer to the comma after the param
1353
     *                                                 // or FALSE if this is the last param.
1354
     *        )
1355
     * </code>
1356
     *
1357
     * Parameters with default values have additional array indexes of:
1358
     *         'default'             => string,  // The full content of the default value.
1359
     *         'default_token'       => integer, // The stack pointer to the start of the default value.
1360
     *         'default_equal_token' => integer, // The stack pointer to the equals sign.
1361
     *
1362
     * Parameters declared using PHP 8 constructor property promotion, have these additional array indexes:
1363
     *         'property_visibility' => string,        // The property visibility as declared.
1364
     *         'visibility_token'    => integer|false, // The stack pointer to the visibility modifier token
1365
     *                                                 // or FALSE if the visibility is not explicitly declared.
1366
     *         'property_readonly'   => boolean,       // TRUE if the readonly keyword was found.
1367
     *         'readonly_token'      => integer,       // The stack pointer to the readonly modifier token.
1368
     *                                                 // This index will only be set if the property is readonly.
1369
     *
1370
     * ... and if the promoted property uses asymmetric visibility, these additional array indexes will also be available:
1371
     *         'set_visibility'       => string,       // The property set-visibility as declared.
1372
     *         'set_visibility_token' => integer,      // The stack pointer to the set-visibility modifier token.
1373
     *
1374
     * @param int $stackPtr The position in the stack of the function token
1375
     *                      to acquire the parameters for.
1376
     *
1377
     * @return array
1378
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified $stackPtr is not of
1379
     *                                                      type T_FUNCTION, T_CLOSURE, T_USE,
1380
     *                                                      or T_FN.
1381
     */
1382
    public function getMethodParameters(int $stackPtr)
240✔
1383
    {
1384
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
240✔
1385
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
240✔
1386
            && $this->tokens[$stackPtr]['code'] !== T_USE
240✔
1387
            && $this->tokens[$stackPtr]['code'] !== T_FN
240✔
1388
        ) {
1389
            throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_USE or T_FN');
9✔
1390
        }
1391

1392
        if ($this->tokens[$stackPtr]['code'] === T_USE
231✔
1393
            && isset($this->tokens[$stackPtr]['parenthesis_owner']) === false
231✔
1394
        ) {
1395
            throw new RuntimeException('$stackPtr was not a valid T_USE');
12✔
1396
        }
1397

1398
        if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === false) {
219✔
1399
            // Live coding or syntax error, so no params to find.
1400
            return [];
3✔
1401
        }
1402

1403
        $opener = $this->tokens[$stackPtr]['parenthesis_opener'];
216✔
1404

1405
        if (isset($this->tokens[$opener]['parenthesis_closer']) === false) {
216✔
1406
            // Live coding or syntax error, so no params to find.
1407
            return [];
6✔
1408
        }
1409

1410
        $closer = $this->tokens[$opener]['parenthesis_closer'];
210✔
1411

1412
        $vars            = [];
210✔
1413
        $currVar         = null;
210✔
1414
        $paramStart      = ($opener + 1);
210✔
1415
        $defaultStart    = null;
210✔
1416
        $equalToken      = null;
210✔
1417
        $paramCount      = 0;
210✔
1418
        $hasAttributes   = false;
210✔
1419
        $passByReference = false;
210✔
1420
        $referenceToken  = false;
210✔
1421
        $variableLength  = false;
210✔
1422
        $variadicToken   = false;
210✔
1423
        $typeHint        = '';
210✔
1424
        $typeHintToken   = false;
210✔
1425
        $typeHintEndToken   = false;
210✔
1426
        $nullableType       = false;
210✔
1427
        $visibilityToken    = null;
210✔
1428
        $setVisibilityToken = null;
210✔
1429
        $readonlyToken      = null;
210✔
1430

1431
        for ($i = $paramStart; $i <= $closer; $i++) {
210✔
1432
            // Check to see if this token has a parenthesis or bracket opener. If it does
1433
            // it's likely to be an array which might have arguments in it. This
1434
            // could cause problems in our parsing below, so lets just skip to the
1435
            // end of it.
1436
            if ($this->tokens[$i]['code'] !== T_TYPE_OPEN_PARENTHESIS
210✔
1437
                && isset($this->tokens[$i]['parenthesis_opener']) === true
210✔
1438
            ) {
1439
                // Don't do this if it's the close parenthesis for the method.
1440
                if ($i !== $this->tokens[$i]['parenthesis_closer']) {
210✔
1441
                    $i = $this->tokens[$i]['parenthesis_closer'];
9✔
1442
                    continue;
9✔
1443
                }
1444
            }
1445

1446
            if (isset($this->tokens[$i]['bracket_opener']) === true) {
210✔
1447
                if ($i !== $this->tokens[$i]['bracket_closer']) {
3✔
1448
                    $i = $this->tokens[$i]['bracket_closer'];
3✔
1449
                    continue;
3✔
1450
                }
1451
            }
1452

1453
            switch ($this->tokens[$i]['code']) {
210✔
1454
                case T_ATTRIBUTE:
210✔
1455
                    $hasAttributes = true;
6✔
1456

1457
                    // Skip to the end of the attribute.
1458
                    $i = $this->tokens[$i]['attribute_closer'];
6✔
1459
                    break;
6✔
1460
                case T_BITWISE_AND:
210✔
1461
                    if ($defaultStart === null) {
54✔
1462
                        $passByReference = true;
51✔
1463
                        $referenceToken  = $i;
51✔
1464
                    }
1465
                    break;
54✔
1466
                case T_VARIABLE:
210✔
1467
                    $currVar = $i;
201✔
1468
                    break;
201✔
1469
                case T_ELLIPSIS:
210✔
1470
                    $variableLength = true;
51✔
1471
                    $variadicToken  = $i;
51✔
1472
                    break;
51✔
1473
                case T_CALLABLE:
210✔
1474
                    if ($typeHintToken === false) {
12✔
1475
                        $typeHintToken = $i;
9✔
1476
                    }
1477

1478
                    $typeHint        .= $this->tokens[$i]['content'];
12✔
1479
                    $typeHintEndToken = $i;
12✔
1480
                    break;
12✔
1481
                case T_SELF:
210✔
1482
                case T_PARENT:
210✔
1483
                case T_STATIC:
210✔
1484
                    // Self and parent are valid, static invalid, but was probably intended as type hint.
1485
                    if (isset($defaultStart) === false) {
18✔
1486
                        if ($typeHintToken === false) {
15✔
1487
                            $typeHintToken = $i;
12✔
1488
                        }
1489

1490
                        $typeHint        .= $this->tokens[$i]['content'];
15✔
1491
                        $typeHintEndToken = $i;
15✔
1492
                    }
1493
                    break;
18✔
1494
                case T_STRING:
210✔
1495
                case T_NAME_QUALIFIED:
210✔
1496
                case T_NAME_FULLY_QUALIFIED:
210✔
1497
                case T_NAME_RELATIVE:
210✔
1498
                    // This is an identifier name, so it may be a type declaration, but it could
1499
                    // also be a constant used as a default value.
1500
                    $prevComma = false;
141✔
1501
                    for ($t = $i; $t >= $opener; $t--) {
141✔
1502
                        if ($this->tokens[$t]['code'] === T_COMMA) {
141✔
1503
                            $prevComma = $t;
63✔
1504
                            break;
63✔
1505
                        }
1506
                    }
1507

1508
                    if ($prevComma !== false) {
141✔
1509
                        $nextEquals = false;
63✔
1510
                        for ($t = $prevComma; $t < $i; $t++) {
63✔
1511
                            if ($this->tokens[$t]['code'] === T_EQUAL) {
63✔
1512
                                $nextEquals = $t;
9✔
1513
                                break;
9✔
1514
                            }
1515
                        }
1516

1517
                        if ($nextEquals !== false) {
63✔
1518
                            break;
9✔
1519
                        }
1520
                    }
1521

1522
                    if ($defaultStart === null) {
138✔
1523
                        if ($typeHintToken === false) {
135✔
1524
                            $typeHintToken = $i;
120✔
1525
                        }
1526

1527
                        $typeHint        .= $this->tokens[$i]['content'];
135✔
1528
                        $typeHintEndToken = $i;
135✔
1529
                    }
1530
                    break;
138✔
1531
                case T_NAMESPACE:
210✔
1532
                case T_NS_SEPARATOR:
210✔
1533
                case T_TYPE_UNION:
210✔
1534
                case T_TYPE_INTERSECTION:
210✔
1535
                case T_TYPE_OPEN_PARENTHESIS:
210✔
1536
                case T_TYPE_CLOSE_PARENTHESIS:
210✔
1537
                case T_FALSE:
210✔
1538
                case T_TRUE:
210✔
1539
                case T_NULL:
210✔
1540
                    // Part of a type hint or default value.
1541
                    if ($defaultStart === null) {
99✔
1542
                        if ($typeHintToken === false) {
90✔
1543
                            $typeHintToken = $i;
27✔
1544
                        }
1545

1546
                        $typeHint        .= $this->tokens[$i]['content'];
90✔
1547
                        $typeHintEndToken = $i;
90✔
1548
                    }
1549
                    break;
99✔
1550
                case T_NULLABLE:
210✔
1551
                    if ($defaultStart === null) {
66✔
1552
                        $nullableType     = true;
66✔
1553
                        $typeHint        .= $this->tokens[$i]['content'];
66✔
1554
                        $typeHintEndToken = $i;
66✔
1555
                    }
1556
                    break;
66✔
1557
                case T_PUBLIC:
210✔
1558
                case T_PROTECTED:
210✔
1559
                case T_PRIVATE:
210✔
1560
                    if ($defaultStart === null) {
27✔
1561
                        $visibilityToken = $i;
27✔
1562
                    }
1563
                    break;
27✔
1564
                case T_PUBLIC_SET:
210✔
1565
                case T_PROTECTED_SET:
210✔
1566
                case T_PRIVATE_SET:
210✔
1567
                    if ($defaultStart === null) {
3✔
1568
                        $setVisibilityToken = $i;
3✔
1569
                    }
1570
                    break;
3✔
1571
                case T_READONLY:
210✔
1572
                    if ($defaultStart === null) {
12✔
1573
                        $readonlyToken = $i;
12✔
1574
                    }
1575
                    break;
12✔
1576
                case T_CLOSE_PARENTHESIS:
210✔
1577
                case T_COMMA:
195✔
1578
                    // If it's null, then there must be no parameters for this
1579
                    // method.
1580
                    if ($currVar === null) {
210✔
1581
                        continue 2;
36✔
1582
                    }
1583

1584
                    $vars[$paramCount]            = [];
201✔
1585
                    $vars[$paramCount]['token']   = $currVar;
201✔
1586
                    $vars[$paramCount]['name']    = $this->tokens[$currVar]['content'];
201✔
1587
                    $vars[$paramCount]['content'] = trim($this->getTokensAsString($paramStart, ($i - $paramStart)));
201✔
1588

1589
                    if ($defaultStart !== null) {
201✔
1590
                        $vars[$paramCount]['default']       = trim($this->getTokensAsString($defaultStart, ($i - $defaultStart)));
66✔
1591
                        $vars[$paramCount]['default_token'] = $defaultStart;
66✔
1592
                        $vars[$paramCount]['default_equal_token'] = $equalToken;
66✔
1593
                    }
1594

1595
                    $vars[$paramCount]['has_attributes']      = $hasAttributes;
201✔
1596
                    $vars[$paramCount]['pass_by_reference']   = $passByReference;
201✔
1597
                    $vars[$paramCount]['reference_token']     = $referenceToken;
201✔
1598
                    $vars[$paramCount]['variable_length']     = $variableLength;
201✔
1599
                    $vars[$paramCount]['variadic_token']      = $variadicToken;
201✔
1600
                    $vars[$paramCount]['type_hint']           = $typeHint;
201✔
1601
                    $vars[$paramCount]['type_hint_token']     = $typeHintToken;
201✔
1602
                    $vars[$paramCount]['type_hint_end_token'] = $typeHintEndToken;
201✔
1603
                    $vars[$paramCount]['nullable_type']       = $nullableType;
201✔
1604

1605
                    if ($visibilityToken !== null || $setVisibilityToken !== null || $readonlyToken !== null) {
201✔
1606
                        $vars[$paramCount]['property_visibility'] = 'public';
30✔
1607
                        $vars[$paramCount]['visibility_token']    = false;
30✔
1608

1609
                        if ($visibilityToken !== null) {
30✔
1610
                            $vars[$paramCount]['property_visibility'] = $this->tokens[$visibilityToken]['content'];
27✔
1611
                            $vars[$paramCount]['visibility_token']    = $visibilityToken;
27✔
1612
                        }
1613

1614
                        if ($setVisibilityToken !== null) {
30✔
1615
                            $vars[$paramCount]['set_visibility']       = $this->tokens[$setVisibilityToken]['content'];
3✔
1616
                            $vars[$paramCount]['set_visibility_token'] = $setVisibilityToken;
3✔
1617
                        }
1618

1619
                        $vars[$paramCount]['property_readonly'] = false;
30✔
1620
                        if ($readonlyToken !== null) {
30✔
1621
                            $vars[$paramCount]['property_readonly'] = true;
12✔
1622
                            $vars[$paramCount]['readonly_token']    = $readonlyToken;
12✔
1623
                        }
1624
                    }
1625

1626
                    if ($this->tokens[$i]['code'] === T_COMMA) {
201✔
1627
                        $vars[$paramCount]['comma_token'] = $i;
102✔
1628
                    } else {
1629
                        $vars[$paramCount]['comma_token'] = false;
174✔
1630
                    }
1631

1632
                    // Reset the vars, as we are about to process the next parameter.
1633
                    $currVar            = null;
201✔
1634
                    $paramStart         = ($i + 1);
201✔
1635
                    $defaultStart       = null;
201✔
1636
                    $equalToken         = null;
201✔
1637
                    $hasAttributes      = false;
201✔
1638
                    $passByReference    = false;
201✔
1639
                    $referenceToken     = false;
201✔
1640
                    $variableLength     = false;
201✔
1641
                    $variadicToken      = false;
201✔
1642
                    $typeHint           = '';
201✔
1643
                    $typeHintToken      = false;
201✔
1644
                    $typeHintEndToken   = false;
201✔
1645
                    $nullableType       = false;
201✔
1646
                    $visibilityToken    = null;
201✔
1647
                    $setVisibilityToken = null;
201✔
1648
                    $readonlyToken      = null;
201✔
1649

1650
                    $paramCount++;
201✔
1651
                    break;
201✔
1652
                case T_EQUAL:
195✔
1653
                    $defaultStart = $this->findNext(Tokens::EMPTY_TOKENS, ($i + 1), null, true);
66✔
1654
                    $equalToken   = $i;
66✔
1655
                    break;
66✔
1656
            }
1657
        }
1658

1659
        return $vars;
210✔
1660
    }
1661

1662

1663
    /**
1664
     * Returns the visibility and implementation properties of a method.
1665
     *
1666
     * The format of the return value is:
1667
     * <code>
1668
     *   array(
1669
     *    'scope'                 => string,        // Public, private, or protected
1670
     *    'scope_specified'       => boolean,       // TRUE if the scope keyword was found.
1671
     *    'return_type'           => string,        // The return type of the method.
1672
     *    'return_type_token'     => integer|false, // The stack pointer to the start of the return type
1673
     *                                              // or FALSE if there is no return type.
1674
     *    'return_type_end_token' => integer|false, // The stack pointer to the end of the return type
1675
     *                                              // or FALSE if there is no return type.
1676
     *    'nullable_return_type'  => boolean,       // TRUE if the return type is preceded by the
1677
     *                                              // nullability operator.
1678
     *    'is_abstract'           => boolean,       // TRUE if the abstract keyword was found.
1679
     *    'is_final'              => boolean,       // TRUE if the final keyword was found.
1680
     *    'is_static'             => boolean,       // TRUE if the static keyword was found.
1681
     *    'has_body'              => boolean,       // TRUE if the method has a body
1682
     *   );
1683
     * </code>
1684
     *
1685
     * @param int $stackPtr The position in the stack of the function token to
1686
     *                      acquire the properties for.
1687
     *
1688
     * @return array
1689
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1690
     *                                                      T_FUNCTION, T_CLOSURE, or T_FN token.
1691
     */
1692
    public function getMethodProperties(int $stackPtr)
177✔
1693
    {
1694
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
177✔
1695
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
177✔
1696
            && $this->tokens[$stackPtr]['code'] !== T_FN
177✔
1697
        ) {
1698
            throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_FN');
9✔
1699
        }
1700

1701
        if ($this->tokens[$stackPtr]['code'] === T_FUNCTION) {
168✔
1702
            $valid = [
82✔
1703
                T_PUBLIC     => T_PUBLIC,
123✔
1704
                T_PRIVATE    => T_PRIVATE,
123✔
1705
                T_PROTECTED  => T_PROTECTED,
123✔
1706
                T_STATIC     => T_STATIC,
123✔
1707
                T_FINAL      => T_FINAL,
123✔
1708
                T_ABSTRACT   => T_ABSTRACT,
123✔
1709
                T_WHITESPACE => T_WHITESPACE,
123✔
1710
                T_COMMENT    => T_COMMENT,
123✔
1711
            ];
82✔
1712
        } else {
1713
            $valid = [
30✔
1714
                T_STATIC     => T_STATIC,
45✔
1715
                T_WHITESPACE => T_WHITESPACE,
45✔
1716
                T_COMMENT    => T_COMMENT,
45✔
1717
            ];
30✔
1718
        }
1719

1720
        $scope          = 'public';
168✔
1721
        $scopeSpecified = false;
168✔
1722
        $isAbstract     = false;
168✔
1723
        $isFinal        = false;
168✔
1724
        $isStatic       = false;
168✔
1725

1726
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
168✔
1727
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
168✔
1728
                break;
165✔
1729
            }
1730

1731
            switch ($this->tokens[$i]['code']) {
165✔
1732
                case T_PUBLIC:
165✔
1733
                    $scope          = 'public';
18✔
1734
                    $scopeSpecified = true;
18✔
1735
                    break;
18✔
1736
                case T_PRIVATE:
165✔
1737
                    $scope          = 'private';
9✔
1738
                    $scopeSpecified = true;
9✔
1739
                    break;
9✔
1740
                case T_PROTECTED:
165✔
1741
                    $scope          = 'protected';
9✔
1742
                    $scopeSpecified = true;
9✔
1743
                    break;
9✔
1744
                case T_ABSTRACT:
165✔
1745
                    $isAbstract = true;
9✔
1746
                    break;
9✔
1747
                case T_FINAL:
165✔
1748
                    $isFinal = true;
3✔
1749
                    break;
3✔
1750
                case T_STATIC:
165✔
1751
                    $isStatic = true;
6✔
1752
                    break;
6✔
1753
            }
1754
        }
1755

1756
        $returnType         = '';
168✔
1757
        $returnTypeToken    = false;
168✔
1758
        $returnTypeEndToken = false;
168✔
1759
        $nullableReturnType = false;
168✔
1760
        $hasBody            = true;
168✔
1761

1762
        if (isset($this->tokens[$stackPtr]['parenthesis_closer']) === true) {
168✔
1763
            $scopeOpener = null;
168✔
1764
            if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
168✔
1765
                $scopeOpener = $this->tokens[$stackPtr]['scope_opener'];
153✔
1766
            }
1767

1768
            $valid  = Tokens::NAME_TOKENS;
168✔
1769
            $valid += [
112✔
1770
                T_CALLABLE               => T_CALLABLE,
168✔
1771
                T_SELF                   => T_SELF,
168✔
1772
                T_PARENT                 => T_PARENT,
168✔
1773
                T_STATIC                 => T_STATIC,
168✔
1774
                T_FALSE                  => T_FALSE,
168✔
1775
                T_TRUE                   => T_TRUE,
168✔
1776
                T_NULL                   => T_NULL,
168✔
1777
                T_TYPE_UNION             => T_TYPE_UNION,
168✔
1778
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
168✔
1779
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
168✔
1780
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
168✔
1781
            ];
112✔
1782

1783
            for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) {
168✔
1784
                if (($scopeOpener === null && $this->tokens[$i]['code'] === T_SEMICOLON)
168✔
1785
                    || ($scopeOpener !== null && $i === $scopeOpener)
168✔
1786
                ) {
1787
                    // End of function definition.
1788
                    break;
168✔
1789
                }
1790

1791
                // Skip over closure use statements.
1792
                if ($this->tokens[$i]['code'] === T_USE) {
168✔
1793
                    if (isset($this->tokens[$i]['parenthesis_closer']) === false) {
15✔
1794
                        // Live coding/parse error, stop parsing.
1795
                        break;
×
1796
                    }
1797

1798
                    $i = $this->tokens[$i]['parenthesis_closer'];
15✔
1799
                    continue;
15✔
1800
                }
1801

1802
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
168✔
1803
                    $nullableReturnType = true;
39✔
1804
                }
1805

1806
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
168✔
1807
                    if ($returnTypeToken === false) {
144✔
1808
                        $returnTypeToken = $i;
144✔
1809
                    }
1810

1811
                    $returnType        .= $this->tokens[$i]['content'];
144✔
1812
                    $returnTypeEndToken = $i;
144✔
1813
                }
1814
            }
1815

1816
            if ($this->tokens[$stackPtr]['code'] === T_FN) {
168✔
1817
                $bodyToken = T_FN_ARROW;
18✔
1818
            } else {
1819
                $bodyToken = T_OPEN_CURLY_BRACKET;
150✔
1820
            }
1821

1822
            $end     = $this->findNext([$bodyToken, T_SEMICOLON], $this->tokens[$stackPtr]['parenthesis_closer']);
168✔
1823
            $hasBody = $this->tokens[$end]['code'] === $bodyToken;
168✔
1824
        }
1825

1826
        if ($returnType !== '' && $nullableReturnType === true) {
168✔
1827
            $returnType = '?' . $returnType;
39✔
1828
        }
1829

1830
        return [
112✔
1831
            'scope'                 => $scope,
168✔
1832
            'scope_specified'       => $scopeSpecified,
168✔
1833
            'return_type'           => $returnType,
168✔
1834
            'return_type_token'     => $returnTypeToken,
168✔
1835
            'return_type_end_token' => $returnTypeEndToken,
168✔
1836
            'nullable_return_type'  => $nullableReturnType,
168✔
1837
            'is_abstract'           => $isAbstract,
168✔
1838
            'is_final'              => $isFinal,
168✔
1839
            'is_static'             => $isStatic,
168✔
1840
            'has_body'              => $hasBody,
168✔
1841
        ];
112✔
1842
    }
1843

1844

1845
    /**
1846
     * Returns the visibility and implementation properties of a class member var.
1847
     *
1848
     * The format of the return value is:
1849
     *
1850
     * <code>
1851
     *   array(
1852
     *    'scope'           => string,        // Public, private, or protected.
1853
     *    'scope_specified' => boolean,       // TRUE if the scope was explicitly specified.
1854
     *    'set_scope'       => string|false,  // Scope for asymmetric visibility.
1855
     *                                        // Either public, private, or protected or
1856
     *                                        // FALSE if no set scope is specified.
1857
     *    'is_static'       => boolean,       // TRUE if the static keyword was found.
1858
     *    'is_readonly'     => boolean,       // TRUE if the readonly keyword was found.
1859
     *    'is_final'        => boolean,       // TRUE if the final keyword was found.
1860
     *    'is_abstract'     => boolean,       // TRUE if the abstract keyword was found.
1861
     *    'type'            => string,        // The type of the var (empty if no type specified).
1862
     *    'type_token'      => integer|false, // The stack pointer to the start of the type
1863
     *                                        // or FALSE if there is no type.
1864
     *    'type_end_token'  => integer|false, // The stack pointer to the end of the type
1865
     *                                        // or FALSE if there is no type.
1866
     *    'nullable_type'   => boolean,       // TRUE if the type is preceded by the nullability
1867
     *                                        // operator.
1868
     *   );
1869
     * </code>
1870
     *
1871
     * @param int $stackPtr The position in the stack of the T_VARIABLE token to
1872
     *                      acquire the properties for.
1873
     *
1874
     * @return array
1875
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1876
     *                                                      T_VARIABLE token, or if the position is not
1877
     *                                                      a class member variable.
1878
     */
1879
    public function getMemberProperties(int $stackPtr)
360✔
1880
    {
1881
        if ($this->tokens[$stackPtr]['code'] !== T_VARIABLE) {
360✔
1882
            throw new RuntimeException('$stackPtr must be of type T_VARIABLE');
3✔
1883
        }
1884

1885
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
357✔
1886
            throw new RuntimeException('$stackPtr is not a class member var');
3✔
1887
        }
1888

1889
        $conditions = $this->tokens[$stackPtr]['conditions'];
354✔
1890
        $conditions = array_keys($conditions);
354✔
1891
        $ptr        = array_pop($conditions);
354✔
1892
        if (isset($this->tokens[$ptr]) === false
354✔
1893
            || isset(Tokens::OO_SCOPE_TOKENS[$this->tokens[$ptr]['code']]) === false
354✔
1894
            || $this->tokens[$ptr]['code'] === T_ENUM
354✔
1895
        ) {
1896
            throw new RuntimeException('$stackPtr is not a class member var');
12✔
1897
        }
1898

1899
        // Make sure it's not a method parameter.
1900
        if (empty($this->tokens[$stackPtr]['nested_parenthesis']) === false) {
342✔
1901
            $parenthesis = array_keys($this->tokens[$stackPtr]['nested_parenthesis']);
15✔
1902
            $deepestOpen = array_pop($parenthesis);
15✔
1903
            if ($deepestOpen > $ptr
15✔
1904
                && isset($this->tokens[$deepestOpen]['parenthesis_owner']) === true
15✔
1905
                && $this->tokens[$this->tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION
15✔
1906
            ) {
1907
                throw new RuntimeException('$stackPtr is not a class member var');
9✔
1908
            }
1909
        }
1910

1911
        $valid = [
222✔
1912
            T_STATIC   => T_STATIC,
333✔
1913
            T_VAR      => T_VAR,
333✔
1914
            T_READONLY => T_READONLY,
333✔
1915
            T_FINAL    => T_FINAL,
333✔
1916
            T_ABSTRACT => T_ABSTRACT,
333✔
1917
        ];
222✔
1918

1919
        $valid += Tokens::SCOPE_MODIFIERS;
333✔
1920
        $valid += Tokens::EMPTY_TOKENS;
333✔
1921

1922
        $scope          = 'public';
333✔
1923
        $scopeSpecified = false;
333✔
1924
        $setScope       = false;
333✔
1925
        $isStatic       = false;
333✔
1926
        $isReadonly     = false;
333✔
1927
        $isFinal        = false;
333✔
1928
        $isAbstract     = false;
333✔
1929

1930
        $startOfStatement = $this->findPrevious(
333✔
1931
            [
222✔
1932
                T_SEMICOLON,
333✔
1933
                T_OPEN_CURLY_BRACKET,
333✔
1934
                T_CLOSE_CURLY_BRACKET,
333✔
1935
                T_ATTRIBUTE_END,
333✔
1936
            ],
222✔
1937
            ($stackPtr - 1)
333✔
1938
        );
222✔
1939

1940
        for ($i = ($startOfStatement + 1); $i < $stackPtr; $i++) {
333✔
1941
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
333✔
1942
                break;
261✔
1943
            }
1944

1945
            switch ($this->tokens[$i]['code']) {
333✔
1946
                case T_PUBLIC:
333✔
1947
                    $scope          = 'public';
162✔
1948
                    $scopeSpecified = true;
162✔
1949
                    break;
162✔
1950
                case T_PRIVATE:
333✔
1951
                    $scope          = 'private';
60✔
1952
                    $scopeSpecified = true;
60✔
1953
                    break;
60✔
1954
                case T_PROTECTED:
333✔
1955
                    $scope          = 'protected';
54✔
1956
                    $scopeSpecified = true;
54✔
1957
                    break;
54✔
1958
                case T_PUBLIC_SET:
333✔
1959
                    $setScope = 'public';
9✔
1960
                    break;
9✔
1961
                case T_PROTECTED_SET:
333✔
1962
                    $setScope = 'protected';
12✔
1963
                    break;
12✔
1964
                case T_PRIVATE_SET:
333✔
1965
                    $setScope = 'private';
9✔
1966
                    break;
9✔
1967
                case T_STATIC:
333✔
1968
                    $isStatic = true;
72✔
1969
                    break;
72✔
1970
                case T_READONLY:
333✔
1971
                    $isReadonly = true;
42✔
1972
                    break;
42✔
1973
                case T_FINAL:
333✔
1974
                    $isFinal = true;
30✔
1975
                    break;
30✔
1976
                case T_ABSTRACT:
333✔
1977
                    $isAbstract = true;
30✔
1978
                    break;
30✔
1979
            }
1980
        }
1981

1982
        $type         = '';
333✔
1983
        $typeToken    = false;
333✔
1984
        $typeEndToken = false;
333✔
1985
        $nullableType = false;
333✔
1986

1987
        if ($i < $stackPtr) {
333✔
1988
            // We've found a type.
1989
            $valid  = Tokens::NAME_TOKENS;
261✔
1990
            $valid += [
174✔
1991
                T_CALLABLE               => T_CALLABLE,
261✔
1992
                T_SELF                   => T_SELF,
261✔
1993
                T_PARENT                 => T_PARENT,
261✔
1994
                T_FALSE                  => T_FALSE,
261✔
1995
                T_TRUE                   => T_TRUE,
261✔
1996
                T_NULL                   => T_NULL,
261✔
1997
                T_TYPE_UNION             => T_TYPE_UNION,
261✔
1998
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
261✔
1999
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
261✔
2000
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
261✔
2001
            ];
174✔
2002

2003
            for ($i; $i < $stackPtr; $i++) {
261✔
2004
                if ($this->tokens[$i]['code'] === T_VARIABLE) {
261✔
2005
                    // Hit another variable in a group definition.
2006
                    break;
30✔
2007
                }
2008

2009
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
237✔
2010
                    $nullableType = true;
57✔
2011
                }
2012

2013
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
237✔
2014
                    $typeEndToken = $i;
237✔
2015
                    if ($typeToken === false) {
237✔
2016
                        $typeToken = $i;
237✔
2017
                    }
2018

2019
                    $type .= $this->tokens[$i]['content'];
237✔
2020
                }
2021
            }
2022

2023
            if ($type !== '' && $nullableType === true) {
261✔
2024
                $type = '?' . $type;
57✔
2025
            }
2026
        }
2027

2028
        return [
222✔
2029
            'scope'           => $scope,
333✔
2030
            'scope_specified' => $scopeSpecified,
333✔
2031
            'set_scope'       => $setScope,
333✔
2032
            'is_static'       => $isStatic,
333✔
2033
            'is_readonly'     => $isReadonly,
333✔
2034
            'is_final'        => $isFinal,
333✔
2035
            'is_abstract'     => $isAbstract,
333✔
2036
            'type'            => $type,
333✔
2037
            'type_token'      => $typeToken,
333✔
2038
            'type_end_token'  => $typeEndToken,
333✔
2039
            'nullable_type'   => $nullableType,
333✔
2040
        ];
222✔
2041
    }
2042

2043

2044
    /**
2045
     * Returns the visibility and implementation properties of a class.
2046
     *
2047
     * The format of the return value is:
2048
     * <code>
2049
     *   array(
2050
     *    'is_abstract' => boolean, // TRUE if the abstract keyword was found.
2051
     *    'is_final'    => boolean, // TRUE if the final keyword was found.
2052
     *    'is_readonly' => boolean, // TRUE if the readonly keyword was found.
2053
     *   );
2054
     * </code>
2055
     *
2056
     * @param int $stackPtr The position in the stack of the T_CLASS token to
2057
     *                      acquire the properties for.
2058
     *
2059
     * @return array
2060
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
2061
     *                                                      T_CLASS token.
2062
     */
2063
    public function getClassProperties(int $stackPtr)
42✔
2064
    {
2065
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS) {
42✔
2066
            throw new RuntimeException('$stackPtr must be of type T_CLASS');
9✔
2067
        }
2068

2069
        $valid = [
22✔
2070
            T_FINAL      => T_FINAL,
33✔
2071
            T_ABSTRACT   => T_ABSTRACT,
33✔
2072
            T_READONLY   => T_READONLY,
33✔
2073
            T_WHITESPACE => T_WHITESPACE,
33✔
2074
            T_COMMENT    => T_COMMENT,
33✔
2075
        ];
22✔
2076

2077
        $isAbstract = false;
33✔
2078
        $isFinal    = false;
33✔
2079
        $isReadonly = false;
33✔
2080

2081
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
33✔
2082
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
33✔
2083
                break;
33✔
2084
            }
2085

2086
            switch ($this->tokens[$i]['code']) {
33✔
2087
                case T_ABSTRACT:
33✔
2088
                    $isAbstract = true;
15✔
2089
                    break;
15✔
2090

2091
                case T_FINAL:
33✔
2092
                    $isFinal = true;
12✔
2093
                    break;
12✔
2094

2095
                case T_READONLY:
33✔
2096
                    $isReadonly = true;
15✔
2097
                    break;
15✔
2098
            }
2099
        }
2100

2101
        return [
22✔
2102
            'is_abstract' => $isAbstract,
33✔
2103
            'is_final'    => $isFinal,
33✔
2104
            'is_readonly' => $isReadonly,
33✔
2105
        ];
22✔
2106
    }
2107

2108

2109
    /**
2110
     * Determine if the passed token is a reference operator.
2111
     *
2112
     * Returns true if the specified token position represents a reference.
2113
     * Returns false if the token represents a bitwise operator.
2114
     *
2115
     * @param int $stackPtr The position of the T_BITWISE_AND token.
2116
     *
2117
     * @return boolean
2118
     */
2119
    public function isReference(int $stackPtr)
228✔
2120
    {
2121
        if ($this->tokens[$stackPtr]['code'] !== T_BITWISE_AND) {
228✔
2122
            return false;
9✔
2123
        }
2124

2125
        $tokenBefore = $this->findPrevious(
219✔
2126
            Tokens::EMPTY_TOKENS,
219✔
2127
            ($stackPtr - 1),
219✔
2128
            null,
219✔
2129
            true
219✔
2130
        );
146✔
2131

2132
        if ($this->tokens[$tokenBefore]['code'] === T_FUNCTION
219✔
2133
            || $this->tokens[$tokenBefore]['code'] === T_CLOSURE
216✔
2134
            || $this->tokens[$tokenBefore]['code'] === T_FN
219✔
2135
        ) {
2136
            // Function returns a reference.
2137
            return true;
9✔
2138
        }
2139

2140
        if ($this->tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
210✔
2141
            // Inside a foreach loop or array assignment, this is a reference.
2142
            return true;
18✔
2143
        }
2144

2145
        if ($this->tokens[$tokenBefore]['code'] === T_AS) {
192✔
2146
            // Inside a foreach loop, this is a reference.
2147
            return true;
3✔
2148
        }
2149

2150
        if (isset(Tokens::ASSIGNMENT_TOKENS[$this->tokens[$tokenBefore]['code']]) === true) {
189✔
2151
            // This is directly after an assignment. It's a reference. Even if
2152
            // it is part of an operation, the other tests will handle it.
2153
            return true;
21✔
2154
        }
2155

2156
        $tokenAfter = $this->findNext(
168✔
2157
            Tokens::EMPTY_TOKENS,
168✔
2158
            ($stackPtr + 1),
168✔
2159
            null,
168✔
2160
            true
168✔
2161
        );
112✔
2162

2163
        if ($this->tokens[$tokenAfter]['code'] === T_NEW) {
168✔
2164
            return true;
3✔
2165
        }
2166

2167
        if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === true) {
165✔
2168
            $brackets    = $this->tokens[$stackPtr]['nested_parenthesis'];
117✔
2169
            $lastBracket = array_pop($brackets);
117✔
2170
            if (isset($this->tokens[$lastBracket]['parenthesis_owner']) === true) {
117✔
2171
                $owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']];
81✔
2172
                if ($owner['code'] === T_FUNCTION
81✔
2173
                    || $owner['code'] === T_CLOSURE
69✔
2174
                    || $owner['code'] === T_FN
42✔
2175
                    || $owner['code'] === T_USE
81✔
2176
                ) {
2177
                    $params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']);
60✔
2178
                    foreach ($params as $param) {
60✔
2179
                        if ($param['reference_token'] === $stackPtr) {
60✔
2180
                            // Function parameter declared to be passed by reference.
2181
                            return true;
45✔
2182
                        }
2183
                    }
2184
                }
2185
            }
2186
        }
2187

2188
        // Pass by reference in function calls and assign by reference in arrays.
2189
        if ($this->tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
120✔
2190
            || $this->tokens[$tokenBefore]['code'] === T_COMMA
108✔
2191
            || $this->tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY
120✔
2192
        ) {
2193
            if ($this->tokens[$tokenAfter]['code'] === T_VARIABLE) {
84✔
2194
                return true;
60✔
2195
            } else {
2196
                $skip   = Tokens::EMPTY_TOKENS;
24✔
2197
                $skip  += Tokens::NAME_TOKENS;
24✔
2198
                $skip[] = T_SELF;
24✔
2199
                $skip[] = T_PARENT;
24✔
2200
                $skip[] = T_STATIC;
24✔
2201
                $skip[] = T_DOUBLE_COLON;
24✔
2202

2203
                $nextSignificantAfter = $this->findNext(
24✔
2204
                    $skip,
24✔
2205
                    ($stackPtr + 1),
24✔
2206
                    null,
24✔
2207
                    true
24✔
2208
                );
16✔
2209
                if ($this->tokens[$nextSignificantAfter]['code'] === T_VARIABLE) {
24✔
2210
                    return true;
24✔
2211
                }
2212
            }
2213
        }
2214

2215
        return false;
36✔
2216
    }
2217

2218

2219
    /**
2220
     * Returns the content of the tokens from the specified start position in
2221
     * the token stack for the specified length.
2222
     *
2223
     * @param int  $start       The position to start from in the token stack.
2224
     * @param int  $length      The length of tokens to traverse from the start pos.
2225
     * @param bool $origContent Whether the original content or the tab replaced
2226
     *                          content should be used.
2227
     *
2228
     * @return string The token contents.
2229
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position does not exist.
2230
     */
2231
    public function getTokensAsString($start, $length, bool $origContent = false)
84✔
2232
    {
2233
        if (is_int($start) === false || isset($this->tokens[$start]) === false) {
84✔
2234
            throw new RuntimeException('The $start position for getTokensAsString() must exist in the token stack');
6✔
2235
        }
2236

2237
        if (is_int($length) === false || $length <= 0) {
78✔
2238
            return '';
9✔
2239
        }
2240

2241
        $str = '';
69✔
2242
        $end = ($start + $length);
69✔
2243
        if ($end > $this->numTokens) {
69✔
2244
            $end = $this->numTokens;
3✔
2245
        }
2246

2247
        for ($i = $start; $i < $end; $i++) {
69✔
2248
            // If tabs are being converted to spaces by the tokeniser, the
2249
            // original content should be used instead of the converted content.
2250
            if ($origContent === true && isset($this->tokens[$i]['orig_content']) === true) {
69✔
2251
                $str .= $this->tokens[$i]['orig_content'];
6✔
2252
            } else {
2253
                $str .= $this->tokens[$i]['content'];
69✔
2254
            }
2255
        }
2256

2257
        return $str;
69✔
2258
    }
2259

2260

2261
    /**
2262
     * Returns the position of the previous specified token(s).
2263
     *
2264
     * If a value is specified, the previous token of the specified type(s)
2265
     * containing the specified value will be returned.
2266
     *
2267
     * Returns false if no token can be found.
2268
     *
2269
     * @param int|string|array $types   The type(s) of tokens to search for.
2270
     * @param int              $start   The position to start searching from in the
2271
     *                                  token stack.
2272
     * @param int|null         $end     The end position to fail if no token is found.
2273
     *                                  if not specified or null, end will default to
2274
     *                                  the start of the token stack.
2275
     * @param bool             $exclude If true, find the previous token that is NOT of
2276
     *                                  the types specified in $types.
2277
     * @param string|null      $value   The value that the token(s) must be equal to.
2278
     *                                  If value is omitted, tokens with any value will
2279
     *                                  be returned.
2280
     * @param bool             $local   If true, tokens outside the current statement
2281
     *                                  will not be checked. IE. checking will stop
2282
     *                                  at the previous semicolon found.
2283
     *
2284
     * @return int|false
2285
     * @see    findNext()
2286
     */
2287
    public function findPrevious(
×
2288
        $types,
2289
        int $start,
2290
        ?int $end = null,
2291
        bool $exclude = false,
2292
        ?string $value = null,
2293
        bool $local = false
2294
    ) {
2295
        $types = (array) $types;
×
2296

2297
        if ($end === null) {
×
2298
            $end = 0;
×
2299
        }
2300

2301
        for ($i = $start; $i >= $end; $i--) {
×
2302
            $found = (bool) $exclude;
×
2303
            foreach ($types as $type) {
×
2304
                if ($this->tokens[$i]['code'] === $type) {
×
2305
                    $found = !$exclude;
×
2306
                    break;
×
2307
                }
2308
            }
2309

2310
            if ($found === true) {
×
2311
                if ($value === null) {
×
2312
                    return $i;
×
2313
                } elseif ($this->tokens[$i]['content'] === $value) {
×
2314
                    return $i;
×
2315
                }
2316
            }
2317

2318
            if ($local === true) {
×
2319
                if (isset($this->tokens[$i]['scope_opener']) === true
×
2320
                    && $i === $this->tokens[$i]['scope_closer']
×
2321
                ) {
2322
                    $i = $this->tokens[$i]['scope_opener'];
×
2323
                } elseif (isset($this->tokens[$i]['bracket_opener']) === true
×
2324
                    && $i === $this->tokens[$i]['bracket_closer']
×
2325
                ) {
2326
                    $i = $this->tokens[$i]['bracket_opener'];
×
2327
                } elseif (isset($this->tokens[$i]['parenthesis_opener']) === true
×
2328
                    && $i === $this->tokens[$i]['parenthesis_closer']
×
2329
                ) {
2330
                    $i = $this->tokens[$i]['parenthesis_opener'];
×
2331
                } elseif ($this->tokens[$i]['code'] === T_SEMICOLON) {
×
2332
                    break;
×
2333
                }
2334
            }
2335
        }
2336

2337
        return false;
×
2338
    }
2339

2340

2341
    /**
2342
     * Returns the position of the next specified token(s).
2343
     *
2344
     * If a value is specified, the next token of the specified type(s)
2345
     * containing the specified value will be returned.
2346
     *
2347
     * Returns false if no token can be found.
2348
     *
2349
     * @param int|string|array $types   The type(s) of tokens to search for.
2350
     * @param int              $start   The position to start searching from in the
2351
     *                                  token stack.
2352
     * @param int|null         $end     The end position to fail if no token is found.
2353
     *                                  if not specified or null, end will default to
2354
     *                                  the end of the token stack.
2355
     * @param bool             $exclude If true, find the next token that is NOT of
2356
     *                                  a type specified in $types.
2357
     * @param string|null      $value   The value that the token(s) must be equal to.
2358
     *                                  If value is omitted, tokens with any value will
2359
     *                                  be returned.
2360
     * @param bool             $local   If true, tokens outside the current statement
2361
     *                                  will not be checked. i.e., checking will stop
2362
     *                                  at the next semicolon found.
2363
     *
2364
     * @return int|false
2365
     * @see    findPrevious()
2366
     */
2367
    public function findNext(
×
2368
        $types,
2369
        int $start,
2370
        ?int $end = null,
2371
        bool $exclude = false,
2372
        ?string $value = null,
2373
        bool $local = false
2374
    ) {
2375
        $types = (array) $types;
×
2376

2377
        if ($end === null || $end > $this->numTokens) {
×
2378
            $end = $this->numTokens;
×
2379
        }
2380

2381
        for ($i = $start; $i < $end; $i++) {
×
2382
            $found = (bool) $exclude;
×
2383
            foreach ($types as $type) {
×
2384
                if ($this->tokens[$i]['code'] === $type) {
×
2385
                    $found = !$exclude;
×
2386
                    break;
×
2387
                }
2388
            }
2389

2390
            if ($found === true) {
×
2391
                if ($value === null) {
×
2392
                    return $i;
×
2393
                } elseif ($this->tokens[$i]['content'] === $value) {
×
2394
                    return $i;
×
2395
                }
2396
            }
2397

2398
            if ($local === true && $this->tokens[$i]['code'] === T_SEMICOLON) {
×
2399
                break;
×
2400
            }
2401
        }
2402

2403
        return false;
×
2404
    }
2405

2406

2407
    /**
2408
     * Returns the position of the first non-whitespace token in a statement.
2409
     *
2410
     * @param int                   $start  The position to start searching from in the token stack.
2411
     * @param int|string|array|null $ignore Token types that should not be considered stop points.
2412
     *
2413
     * @return int
2414
     */
2415
    public function findStartOfStatement(int $start, $ignore = null)
213✔
2416
    {
2417
        $startTokens = Tokens::BLOCK_OPENERS;
213✔
2418
        $startTokens[T_OPEN_SHORT_ARRAY]   = true;
213✔
2419
        $startTokens[T_OPEN_TAG]           = true;
213✔
2420
        $startTokens[T_OPEN_TAG_WITH_ECHO] = true;
213✔
2421

2422
        $endTokens = [
142✔
2423
            T_CLOSE_TAG    => true,
213✔
2424
            T_COLON        => true,
213✔
2425
            T_COMMA        => true,
213✔
2426
            T_DOUBLE_ARROW => true,
213✔
2427
            T_MATCH_ARROW  => true,
213✔
2428
            T_SEMICOLON    => true,
213✔
2429
        ];
142✔
2430

2431
        if ($ignore !== null) {
213✔
2432
            $ignore = (array) $ignore;
×
2433
            foreach ($ignore as $code) {
×
2434
                if (isset($startTokens[$code]) === true) {
×
2435
                    unset($startTokens[$code]);
×
2436
                }
2437

2438
                if (isset($endTokens[$code]) === true) {
×
2439
                    unset($endTokens[$code]);
×
2440
                }
2441
            }
2442
        }
2443

2444
        // If the start token is inside the case part of a match expression,
2445
        // find the start of the condition. If it's in the statement part, find
2446
        // the token that comes after the match arrow.
2447
        if (empty($this->tokens[$start]['conditions']) === false) {
213✔
2448
            $conditions         = $this->tokens[$start]['conditions'];
165✔
2449
            $lastConditionOwner = end($conditions);
165✔
2450
            $matchExpression    = key($conditions);
165✔
2451

2452
            if ($lastConditionOwner === T_MATCH
165✔
2453
                // Check if the $start token is at the same parentheses nesting level as the match token.
2454
                && ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === true
141✔
2455
                && empty($this->tokens[$start]['nested_parenthesis']) === true)
133✔
2456
                || ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === false
127✔
2457
                && empty($this->tokens[$start]['nested_parenthesis']) === false)
127✔
2458
                && $this->tokens[$matchExpression]['nested_parenthesis'] === $this->tokens[$start]['nested_parenthesis']))
165✔
2459
            ) {
2460
                // Walk back to the previous match arrow (if it exists).
2461
                $lastComma          = null;
45✔
2462
                $inNestedExpression = false;
45✔
2463
                for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
45✔
2464
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_MATCH_ARROW) {
45✔
2465
                        break;
33✔
2466
                    }
2467

2468
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_COMMA) {
45✔
2469
                        $lastComma = $prevMatch;
24✔
2470
                        continue;
24✔
2471
                    }
2472

2473
                    // Skip nested statements.
2474
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
45✔
2475
                        && $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
45✔
2476
                    ) {
2477
                        $prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
12✔
2478
                        continue;
12✔
2479
                    }
2480

2481
                    if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
45✔
2482
                        && $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
45✔
2483
                    ) {
2484
                        $prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
15✔
2485
                        continue;
15✔
2486
                    }
2487

2488
                    // Stop if we're _within_ a nested short array statement, which may contain comma's too.
2489
                    // No need to deal with parentheses, those are handled above via the `nested_parenthesis` checks.
2490
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
45✔
2491
                        && $this->tokens[$prevMatch]['bracket_closer'] > $start
45✔
2492
                    ) {
2493
                        $inNestedExpression = true;
15✔
2494
                        break;
15✔
2495
                    }
2496
                }
2497

2498
                if ($inNestedExpression === false) {
45✔
2499
                    // $prevMatch will now either be the scope opener or a match arrow.
2500
                    // If it is the scope opener, go the first non-empty token after. $start will have been part of the first condition.
2501
                    if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) {
33✔
2502
                        // We're before the arrow in the first case.
2503
                        $next = $this->findNext(Tokens::EMPTY_TOKENS, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
12✔
2504
                        if ($next === false) {
12✔
2505
                            // Shouldn't be possible.
2506
                            return $start;
×
2507
                        }
2508

2509
                        return $next;
12✔
2510
                    }
2511

2512
                    // Okay, so we found a match arrow.
2513
                    // If $start was part of the "next" condition, the last comma will be set.
2514
                    // Otherwise, $start must have been part of a return expression.
2515
                    if (isset($lastComma) === true && $lastComma > $prevMatch) {
33✔
2516
                        $prevMatch = $lastComma;
15✔
2517
                    }
2518

2519
                    // In both cases, go to the first non-empty token after.
2520
                    $next = $this->findNext(Tokens::EMPTY_TOKENS, ($prevMatch + 1), null, true);
33✔
2521
                    if ($next === false) {
33✔
2522
                        // Shouldn't be possible.
2523
                        return $start;
×
2524
                    }
2525

2526
                    return $next;
33✔
2527
                }
2528
            }
2529
        }
2530

2531
        $lastNotEmpty = $start;
183✔
2532

2533
        // If we are starting at a token that ends a scope block, skip to
2534
        // the start and continue from there.
2535
        // If we are starting at a token that ends a statement, skip this
2536
        // token so we find the true start of the statement.
2537
        while (isset($endTokens[$this->tokens[$start]['code']]) === true
183✔
2538
            || (isset($this->tokens[$start]['scope_condition']) === true
183✔
2539
            && $start === $this->tokens[$start]['scope_closer'])
183✔
2540
        ) {
2541
            if (isset($this->tokens[$start]['scope_condition']) === true) {
51✔
2542
                $start = $this->tokens[$start]['scope_condition'];
27✔
2543
            } else {
2544
                $start--;
30✔
2545
            }
2546
        }
2547

2548
        for ($i = $start; $i >= 0; $i--) {
183✔
2549
            if (isset($startTokens[$this->tokens[$i]['code']]) === true
183✔
2550
                || isset($endTokens[$this->tokens[$i]['code']]) === true
183✔
2551
            ) {
2552
                // Found the end of the previous statement.
2553
                return $lastNotEmpty;
183✔
2554
            }
2555

2556
            if (isset($this->tokens[$i]['scope_opener']) === true
180✔
2557
                && $i === $this->tokens[$i]['scope_closer']
180✔
2558
                && $this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS
180✔
2559
                && $this->tokens[$i]['code'] !== T_END_NOWDOC
180✔
2560
                && $this->tokens[$i]['code'] !== T_END_HEREDOC
180✔
2561
                && $this->tokens[$i]['code'] !== T_BREAK
180✔
2562
                && $this->tokens[$i]['code'] !== T_RETURN
180✔
2563
                && $this->tokens[$i]['code'] !== T_CONTINUE
180✔
2564
                && $this->tokens[$i]['code'] !== T_THROW
180✔
2565
                && $this->tokens[$i]['code'] !== T_EXIT
180✔
2566
                && $this->tokens[$i]['code'] !== T_GOTO
180✔
2567
            ) {
2568
                // Found the end of the previous scope block.
2569
                return $lastNotEmpty;
3✔
2570
            }
2571

2572
            // Skip nested statements.
2573
            if (isset($this->tokens[$i]['bracket_opener']) === true
180✔
2574
                && $i === $this->tokens[$i]['bracket_closer']
180✔
2575
            ) {
2576
                $i = $this->tokens[$i]['bracket_opener'];
3✔
2577
            } elseif (isset($this->tokens[$i]['parenthesis_opener']) === true
180✔
2578
                && $i === $this->tokens[$i]['parenthesis_closer']
180✔
2579
            ) {
2580
                $i = $this->tokens[$i]['parenthesis_opener'];
24✔
2581
            } elseif ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
180✔
2582
                $start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
6✔
2583
                if ($start !== false) {
6✔
2584
                    $i = $start;
6✔
2585
                }
2586
            }
2587

2588
            if (isset(Tokens::EMPTY_TOKENS[$this->tokens[$i]['code']]) === false) {
180✔
2589
                $lastNotEmpty = $i;
180✔
2590
            }
2591
        }
2592

2593
        return 0;
×
2594
    }
2595

2596

2597
    /**
2598
     * Returns the position of the last non-whitespace token in a statement.
2599
     *
2600
     * @param int                   $start  The position to start searching from in the token stack.
2601
     * @param int|string|array|null $ignore Token types that should not be considered stop points.
2602
     *
2603
     * @return int
2604
     */
2605
    public function findEndOfStatement(int $start, $ignore = null)
66✔
2606
    {
2607
        $endTokens = [
44✔
2608
            T_COLON                => true,
66✔
2609
            T_COMMA                => true,
66✔
2610
            T_DOUBLE_ARROW         => true,
66✔
2611
            T_SEMICOLON            => true,
66✔
2612
            T_CLOSE_PARENTHESIS    => true,
66✔
2613
            T_CLOSE_SQUARE_BRACKET => true,
66✔
2614
            T_CLOSE_CURLY_BRACKET  => true,
66✔
2615
            T_CLOSE_SHORT_ARRAY    => true,
66✔
2616
            T_OPEN_TAG             => true,
66✔
2617
            T_CLOSE_TAG            => true,
66✔
2618
        ];
44✔
2619

2620
        if ($ignore !== null) {
66✔
2621
            $ignore = (array) $ignore;
×
2622
            foreach ($ignore as $code) {
×
2623
                unset($endTokens[$code]);
×
2624
            }
2625
        }
2626

2627
        // If the start token is inside the case part of a match expression,
2628
        // advance to the match arrow and continue looking for the
2629
        // end of the statement from there so that we skip over commas.
2630
        if ($this->tokens[$start]['code'] !== T_MATCH_ARROW) {
66✔
2631
            $matchExpression = $this->getCondition($start, T_MATCH);
66✔
2632
            if ($matchExpression !== false) {
66✔
2633
                $beforeArrow    = true;
30✔
2634
                $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']);
30✔
2635
                if ($prevMatchArrow !== false) {
30✔
2636
                    $prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start);
27✔
2637
                    if ($prevComma === false) {
27✔
2638
                        // No comma between this token and the last match arrow,
2639
                        // so this token exists after the arrow and we can continue
2640
                        // checking as normal.
2641
                        $beforeArrow = false;
12✔
2642
                    }
2643
                }
2644

2645
                if ($beforeArrow === true) {
30✔
2646
                    $nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
30✔
2647
                    if ($nextMatchArrow !== false) {
30✔
2648
                        $start = $nextMatchArrow;
30✔
2649
                    }
2650
                }
2651
            }
2652
        }
2653

2654
        $lastNotEmpty = $start;
66✔
2655
        for ($i = $start; $i < $this->numTokens; $i++) {
66✔
2656
            if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
66✔
2657
                // Found the end of the statement.
2658
                if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS
60✔
2659
                    || $this->tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
57✔
2660
                    || $this->tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
57✔
2661
                    || $this->tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
51✔
2662
                    || $this->tokens[$i]['code'] === T_OPEN_TAG
48✔
2663
                    || $this->tokens[$i]['code'] === T_CLOSE_TAG
60✔
2664
                ) {
2665
                    return $lastNotEmpty;
24✔
2666
                }
2667

2668
                return $i;
48✔
2669
            }
2670

2671
            // Skip nested statements.
2672
            if (isset($this->tokens[$i]['scope_closer']) === true
66✔
2673
                && ($i === $this->tokens[$i]['scope_opener']
56✔
2674
                || $i === $this->tokens[$i]['scope_condition'])
66✔
2675
            ) {
2676
                if ($this->tokens[$i]['code'] === T_FN) {
36✔
2677
                    $lastNotEmpty = $this->tokens[$i]['scope_closer'];
18✔
2678
                    $i            = ($this->tokens[$i]['scope_closer'] - 1);
18✔
2679
                    continue;
18✔
2680
                }
2681

2682
                if ($i === $start && isset(Tokens::SCOPE_OPENERS[$this->tokens[$i]['code']]) === true) {
21✔
2683
                    return $this->tokens[$i]['scope_closer'];
9✔
2684
                }
2685

2686
                $i = $this->tokens[$i]['scope_closer'];
15✔
2687
            } elseif (isset($this->tokens[$i]['bracket_closer']) === true
48✔
2688
                && $i === $this->tokens[$i]['bracket_opener']
48✔
2689
            ) {
2690
                $i = $this->tokens[$i]['bracket_closer'];
6✔
2691
            } elseif (isset($this->tokens[$i]['parenthesis_closer']) === true
48✔
2692
                && $i === $this->tokens[$i]['parenthesis_opener']
48✔
2693
            ) {
2694
                $i = $this->tokens[$i]['parenthesis_closer'];
9✔
2695
            } elseif ($this->tokens[$i]['code'] === T_OPEN_USE_GROUP) {
48✔
2696
                $end = $this->findNext(T_CLOSE_USE_GROUP, ($i + 1));
6✔
2697
                if ($end !== false) {
6✔
2698
                    $i = $end;
6✔
2699
                }
2700
            }
2701

2702
            if (isset(Tokens::EMPTY_TOKENS[$this->tokens[$i]['code']]) === false) {
48✔
2703
                $lastNotEmpty = $i;
48✔
2704
            }
2705
        }
2706

2707
        return ($this->numTokens - 1);
3✔
2708
    }
2709

2710

2711
    /**
2712
     * Returns the position of the first token on a line, matching given type.
2713
     *
2714
     * Returns false if no token can be found.
2715
     *
2716
     * @param int|string|array $types   The type(s) of tokens to search for.
2717
     * @param int              $start   The position to start searching from in the
2718
     *                                  token stack.
2719
     * @param bool             $exclude If true, find the token that is NOT of
2720
     *                                  the types specified in $types.
2721
     * @param string|null      $value   The value that the token must be equal to.
2722
     *                                  If value is omitted, tokens with any value will
2723
     *                                  be returned.
2724
     *
2725
     * @return int|false The first token which matches on the line containing the start
2726
     *                   token, between the start of the line and the start token.
2727
     *                   Note: The first token matching might be the start token.
2728
     *                   FALSE when no matching token could be found between the start of
2729
     *                   the line and the start token.
2730
     */
2731
    public function findFirstOnLine($types, int $start, bool $exclude = false, ?string $value = null)
×
2732
    {
2733
        if (is_array($types) === false) {
×
2734
            $types = [$types];
×
2735
        }
2736

2737
        $foundToken = false;
×
2738

2739
        for ($i = $start; $i >= 0; $i--) {
×
2740
            if ($this->tokens[$i]['line'] < $this->tokens[$start]['line']) {
×
2741
                break;
×
2742
            }
2743

2744
            $found = $exclude;
×
2745
            foreach ($types as $type) {
×
2746
                if ($exclude === false) {
×
2747
                    if ($this->tokens[$i]['code'] === $type) {
×
2748
                        $found = true;
×
2749
                        break;
×
2750
                    }
2751
                } else {
2752
                    if ($this->tokens[$i]['code'] === $type) {
×
2753
                        $found = false;
×
2754
                        break;
×
2755
                    }
2756
                }
2757
            }
2758

2759
            if ($found === true) {
×
2760
                if ($value === null) {
×
2761
                    $foundToken = $i;
×
2762
                } elseif ($this->tokens[$i]['content'] === $value) {
×
2763
                    $foundToken = $i;
×
2764
                }
2765
            }
2766
        }
2767

2768
        return $foundToken;
×
2769
    }
2770

2771

2772
    /**
2773
     * Determine if the passed token has a condition of one of the passed types.
2774
     *
2775
     * @param int              $stackPtr The position of the token we are checking.
2776
     * @param int|string|array $types    The type(s) of tokens to search for.
2777
     *
2778
     * @return boolean
2779
     */
2780
    public function hasCondition(int $stackPtr, $types)
21✔
2781
    {
2782
        // Check for the existence of the token.
2783
        if (isset($this->tokens[$stackPtr]) === false) {
21✔
2784
            return false;
3✔
2785
        }
2786

2787
        // Make sure the token has conditions.
2788
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
18✔
2789
            return false;
×
2790
        }
2791

2792
        $types      = (array) $types;
18✔
2793
        $conditions = $this->tokens[$stackPtr]['conditions'];
18✔
2794

2795
        foreach ($types as $type) {
18✔
2796
            if (in_array($type, $conditions, true) === true) {
18✔
2797
                // We found a token with the required type.
2798
                return true;
15✔
2799
            }
2800
        }
2801

2802
        return false;
18✔
2803
    }
2804

2805

2806
    /**
2807
     * Return the position of the condition for the passed token.
2808
     *
2809
     * Returns FALSE if the token does not have the condition.
2810
     *
2811
     * @param int        $stackPtr The position of the token we are checking.
2812
     * @param int|string $type     The type of token to search for.
2813
     * @param bool       $first    If TRUE, will return the matched condition
2814
     *                             furthest away from the passed token.
2815
     *                             If FALSE, will return the matched condition
2816
     *                             closest to the passed token.
2817
     *
2818
     * @return int|false
2819
     */
2820
    public function getCondition(int $stackPtr, $type, bool $first = true)
30✔
2821
    {
2822
        // Check for the existence of the token.
2823
        if (isset($this->tokens[$stackPtr]) === false) {
30✔
2824
            return false;
3✔
2825
        }
2826

2827
        // Make sure the token has conditions.
2828
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
27✔
2829
            return false;
×
2830
        }
2831

2832
        $conditions = $this->tokens[$stackPtr]['conditions'];
27✔
2833
        if ($first === false) {
27✔
2834
            $conditions = array_reverse($conditions, true);
12✔
2835
        }
2836

2837
        foreach ($conditions as $token => $condition) {
27✔
2838
            if ($condition === $type) {
27✔
2839
                return $token;
24✔
2840
            }
2841
        }
2842

2843
        return false;
27✔
2844
    }
2845

2846

2847
    /**
2848
     * Returns the name of the class that the specified class extends.
2849
     * (works for classes, anonymous classes and interfaces)
2850
     *
2851
     * Returns FALSE on error or if there is no extended class name.
2852
     *
2853
     * @param int $stackPtr The stack position of the class.
2854
     *
2855
     * @return string|false
2856
     */
2857
    public function findExtendedClassName(int $stackPtr)
60✔
2858
    {
2859
        // Check for the existence of the token.
2860
        if (isset($this->tokens[$stackPtr]) === false) {
60✔
2861
            return false;
3✔
2862
        }
2863

2864
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
57✔
2865
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
57✔
2866
            && $this->tokens[$stackPtr]['code'] !== T_INTERFACE
57✔
2867
        ) {
2868
            return false;
3✔
2869
        }
2870

2871
        if (isset($this->tokens[$stackPtr]['scope_opener']) === false) {
54✔
2872
            return false;
3✔
2873
        }
2874

2875
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
51✔
2876
        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex);
51✔
2877
        if ($extendsIndex === false) {
51✔
2878
            return false;
9✔
2879
        }
2880

2881
        $find   = Tokens::NAME_TOKENS;
42✔
2882
        $find[] = T_WHITESPACE;
42✔
2883

2884
        $end  = $this->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true);
42✔
2885
        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
42✔
2886
        $name = trim($name);
42✔
2887

2888
        if ($name === '') {
42✔
2889
            return false;
3✔
2890
        }
2891

2892
        return $name;
39✔
2893
    }
2894

2895

2896
    /**
2897
     * Returns the names of the interfaces that the specified class or enum implements.
2898
     *
2899
     * Returns FALSE on error or if there are no implemented interface names.
2900
     *
2901
     * @param int $stackPtr The stack position of the class or enum token.
2902
     *
2903
     * @return array|false
2904
     */
2905
    public function findImplementedInterfaceNames(int $stackPtr)
51✔
2906
    {
2907
        // Check for the existence of the token.
2908
        if (isset($this->tokens[$stackPtr]) === false) {
51✔
2909
            return false;
3✔
2910
        }
2911

2912
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
48✔
2913
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
48✔
2914
            && $this->tokens[$stackPtr]['code'] !== T_ENUM
48✔
2915
        ) {
2916
            return false;
6✔
2917
        }
2918

2919
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
42✔
2920
            return false;
3✔
2921
        }
2922

2923
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
39✔
2924
        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
39✔
2925
        if ($implementsIndex === false) {
39✔
2926
            return false;
6✔
2927
        }
2928

2929
        $find   = Tokens::NAME_TOKENS;
33✔
2930
        $find[] = T_WHITESPACE;
33✔
2931
        $find[] = T_COMMA;
33✔
2932

2933
        $end  = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
33✔
2934
        $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
33✔
2935
        $name = trim($name);
33✔
2936

2937
        if ($name === '') {
33✔
2938
            return false;
3✔
2939
        } else {
2940
            $names = explode(',', $name);
30✔
2941
            $names = array_map('trim', $names);
30✔
2942
            return $names;
30✔
2943
        }
2944
    }
2945
}
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