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

PHPCSStandards / PHP_CodeSniffer / 15253296250

26 May 2025 11:55AM UTC coverage: 78.632% (+0.3%) from 78.375%
15253296250

Pull #1105

github

web-flow
Merge d9441d98f into caf806050
Pull Request #1105: Skip tests when 'git' command is not available

19665 of 25009 relevant lines covered (78.63%)

88.67 hits per line

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

69.29
/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 that can be fixed (first run on a file).
157
     *
158
     * {@internal This should be regarded as an immutable property.}
159
     *
160
     * @var integer
161
     */
162
    private $fixableErrorCountFirstRun;
163

164
    /**
165
     * The original total number of warnings that can be fixed (first run on a file).
166
     *
167
     * {@internal This should be regarded as an immutable property.}
168
     *
169
     * @var integer
170
     */
171
    private $fixableWarningCountFirstRun;
172

173
    /**
174
     * The current total number of errors that can be fixed.
175
     *
176
     * @var integer
177
     */
178
    protected $fixableErrorCount = 0;
179

180
    /**
181
     * The current total number of warnings that can be fixed.
182
     *
183
     * @var integer
184
     */
185
    protected $fixableWarningCount = 0;
186

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

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

208
    /**
209
     * The effective number of warnings that were fixed.
210
     *
211
     * I.e. how many of the originally found warnings were fixed.
212
     *
213
     * @var integer
214
     */
215
    protected $fixedWarningCount = 0;
216

217
    /**
218
     * TRUE if errors are being replayed from the cache.
219
     *
220
     * @var boolean
221
     */
222
    protected $replayingErrors = false;
223

224
    /**
225
     * An array of sniffs that are being ignored.
226
     *
227
     * @var array
228
     */
229
    protected $ignoredListeners = [];
230

231
    /**
232
     * An array of message codes that are being ignored.
233
     *
234
     * @var array
235
     */
236
    protected $ignoredCodes = [];
237

238
    /**
239
     * An array of sniffs listening to this file's processing.
240
     *
241
     * @var \PHP_CodeSniffer\Sniffs\Sniff[]
242
     */
243
    protected $listeners = [];
244

245
    /**
246
     * The class name of the sniff currently processing the file.
247
     *
248
     * @var string
249
     */
250
    protected $activeListener = '';
251

252
    /**
253
     * An array of sniffs being processed and how long they took.
254
     *
255
     * @var array
256
     * @see getListenerTimes()
257
     */
258
    protected $listenerTimes = [];
259

260
    /**
261
     * A cache of often used config settings to improve performance.
262
     *
263
     * Storing them here saves 10k+ calls to __get() in the Config class.
264
     *
265
     * @var array
266
     */
267
    protected $configCache = [];
268

269

270
    /**
271
     * Constructs a file.
272
     *
273
     * @param string                   $path    The absolute path to the file to process.
274
     * @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run.
275
     * @param \PHP_CodeSniffer\Config  $config  The config data for the run.
276
     *
277
     * @return void
278
     */
279
    public function __construct($path, Ruleset $ruleset, Config $config)
×
280
    {
281
        $this->path    = $path;
×
282
        $this->ruleset = $ruleset;
×
283
        $this->config  = $config;
×
284
        $this->fixer   = new Fixer();
×
285

286
        $this->configCache['cache']           = $this->config->cache;
×
287
        $this->configCache['sniffs']          = array_map('strtolower', $this->config->sniffs);
×
288
        $this->configCache['exclude']         = array_map('strtolower', $this->config->exclude);
×
289
        $this->configCache['errorSeverity']   = $this->config->errorSeverity;
×
290
        $this->configCache['warningSeverity'] = $this->config->warningSeverity;
×
291
        $this->configCache['recordErrors']    = $this->config->recordErrors;
×
292
        $this->configCache['trackTime']       = $this->config->trackTime;
×
293
        $this->configCache['ignorePatterns']  = $this->ruleset->ignorePatterns;
×
294
        $this->configCache['includePatterns'] = $this->ruleset->includePatterns;
×
295

296
    }//end __construct()
297

298

299
    /**
300
     * Set the content of the file.
301
     *
302
     * Setting the content also calculates the EOL char being used.
303
     *
304
     * @param string $content The file content.
305
     *
306
     * @return void
307
     */
308
    public function setContent($content)
×
309
    {
310
        $this->content = $content;
×
311
        $this->tokens  = [];
×
312

313
        try {
314
            $this->eolChar = Common::detectLineEndings($content);
×
315
        } catch (RuntimeException $e) {
×
316
            $this->addWarningOnLine($e->getMessage(), 1, 'Internal.DetectLineEndings');
×
317
            return;
×
318
        }
319

320
    }//end setContent()
321

322

323
    /**
324
     * Reloads the content of the file.
325
     *
326
     * By default, we have no idea where our content comes from,
327
     * so we can't do anything.
328
     *
329
     * @return void
330
     */
331
    public function reloadContent()
×
332
    {
333

334
    }//end reloadContent()
×
335

336

337
    /**
338
     * Disables caching of this file.
339
     *
340
     * @return void
341
     */
342
    public function disableCaching()
×
343
    {
344
        $this->configCache['cache'] = false;
×
345

346
    }//end disableCaching()
347

348

349
    /**
350
     * Starts the stack traversal and tells listeners when tokens are found.
351
     *
352
     * @return void
353
     */
354
    public function process()
×
355
    {
356
        if ($this->ignored === true) {
×
357
            return;
×
358
        }
359

360
        $this->errors            = [];
×
361
        $this->warnings          = [];
×
362
        $this->errorCount        = 0;
×
363
        $this->warningCount      = 0;
×
364
        $this->fixableErrorCount = 0;
×
365
        $this->fixableWarningCount = 0;
×
366

367
        $this->parse();
×
368

369
        // Check if tokenizer errors cause this file to be ignored.
370
        if ($this->ignored === true) {
×
371
            return;
×
372
        }
373

374
        $this->fixer->startFile($this);
×
375

376
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
377
            StatusWriter::write('*** START TOKEN PROCESSING ***', 1);
×
378
        }
379

380
        $foundCode        = false;
×
381
        $listenerIgnoreTo = [];
×
382
        $inTests          = defined('PHP_CODESNIFFER_IN_TESTS');
×
383
        $checkAnnotations = $this->config->annotations;
×
384

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

428
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
429
                $type    = $token['type'];
×
430
                $content = Common::prepareForOutput($token['content']);
×
431
                StatusWriter::write("Process token $stackPtr: $type => $content", 2);
×
432
            }
433

434
            if ($token['code'] !== T_INLINE_HTML) {
×
435
                $foundCode = true;
×
436
            }
437

438
            if (isset($this->ruleset->tokenListeners[$token['code']]) === false) {
×
439
                continue;
×
440
            }
441

442
            foreach ($this->ruleset->tokenListeners[$token['code']] as $listenerData) {
×
443
                if (isset($this->ignoredListeners[$listenerData['class']]) === true
×
444
                    || (isset($listenerIgnoreTo[$listenerData['class']]) === true
×
445
                    && $listenerIgnoreTo[$listenerData['class']] > $stackPtr)
×
446
                ) {
447
                    // This sniff is ignoring past this token, or the whole file.
448
                    continue;
×
449
                }
450

451
                $class = $listenerData['class'];
×
452

453
                if (trim($this->path, '\'"') !== 'STDIN') {
×
454
                    // If the file path matches one of our ignore patterns, skip it.
455
                    // While there is support for a type of each pattern
456
                    // (absolute or relative) we don't actually support it here.
457
                    foreach ($listenerData['ignore'] as $pattern) {
×
458
                        // We assume a / directory separator, as do the exclude rules
459
                        // most developers write, so we need a special case for any system
460
                        // that is different.
461
                        if (DIRECTORY_SEPARATOR === '\\') {
×
462
                            $pattern = str_replace('/', '\\\\', $pattern);
×
463
                        }
464

465
                        $pattern = '`'.$pattern.'`i';
×
466
                        if (preg_match($pattern, $this->path) === 1) {
×
467
                            $this->ignoredListeners[$class] = true;
×
468
                            continue(2);
×
469
                        }
470
                    }
471

472
                    // If the file path does not match one of our include patterns, skip it.
473
                    // While there is support for a type of each pattern
474
                    // (absolute or relative) we don't actually support it here.
475
                    if (empty($listenerData['include']) === false) {
×
476
                        $included = false;
×
477
                        foreach ($listenerData['include'] as $pattern) {
×
478
                            // We assume a / directory separator, as do the exclude rules
479
                            // most developers write, so we need a special case for any system
480
                            // that is different.
481
                            if (DIRECTORY_SEPARATOR === '\\') {
×
482
                                $pattern = str_replace('/', '\\\\', $pattern);
×
483
                            }
484

485
                            $pattern = '`'.$pattern.'`i';
×
486
                            if (preg_match($pattern, $this->path) === 1) {
×
487
                                $included = true;
×
488
                                break;
×
489
                            }
490
                        }
491

492
                        if ($included === false) {
×
493
                            $this->ignoredListeners[$class] = true;
×
494
                            continue;
×
495
                        }
496
                    }//end if
497
                }//end if
498

499
                $this->activeListener = $class;
×
500

501
                if ($this->configCache['trackTime'] === true) {
×
502
                    $startTime = microtime(true);
×
503
                }
504

505
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
506
                    StatusWriter::write('Processing '.$this->activeListener.'... ', 3, 0);
×
507
                }
508

509
                $ignoreTo = $this->ruleset->sniffs[$class]->process($this, $stackPtr);
×
510
                if ($ignoreTo !== null) {
×
511
                    $listenerIgnoreTo[$this->activeListener] = $ignoreTo;
×
512
                }
513

514
                if ($this->configCache['trackTime'] === true) {
×
515
                    $timeTaken = (microtime(true) - $startTime);
×
516
                    if (isset($this->listenerTimes[$this->activeListener]) === false) {
×
517
                        $this->listenerTimes[$this->activeListener] = 0;
×
518
                    }
519

520
                    $this->listenerTimes[$this->activeListener] += $timeTaken;
×
521
                }
522

523
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
524
                    $timeTaken = round(($timeTaken), 4);
×
525
                    StatusWriter::write("DONE in $timeTaken seconds");
×
526
                }
527

528
                $this->activeListener = '';
×
529
            }//end foreach
530
        }//end foreach
531

532
        // If short open tags are off but the file being checked uses
533
        // short open tags, the whole content will be inline HTML
534
        // and nothing will be checked. So try and handle this case.
535
        // We don't show this error for STDIN because we can't be sure the content
536
        // actually came directly from the user. It could be something like
537
        // refs from a Git pre-push hook.
538
        if ($foundCode === false && $this->path !== 'STDIN') {
×
539
            $shortTags = (bool) ini_get('short_open_tag');
×
540
            if ($shortTags === false) {
×
541
                $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.';
×
542
                $this->addWarning($error, null, 'Internal.NoCodeFound');
×
543
            }
544
        }
545

546
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
547
            StatusWriter::write('*** END TOKEN PROCESSING ***', 1);
×
548
            StatusWriter::write('*** START SNIFF PROCESSING REPORT ***', 1);
×
549

550
            arsort($this->listenerTimes, SORT_NUMERIC);
×
551
            foreach ($this->listenerTimes as $listener => $timeTaken) {
×
552
                StatusWriter::write("$listener: ".round(($timeTaken), 4).' secs', 1);
×
553
            }
554

555
            StatusWriter::write('*** END SNIFF PROCESSING REPORT ***', 1);
×
556
        }
557

558
        if (isset($this->fixableErrorCountFirstRun, $this->fixableWarningCountFirstRun) === false) {
×
559
            $this->fixableErrorCountFirstRun   = $this->fixableErrorCount;
×
560
            $this->fixableWarningCountFirstRun = $this->fixableWarningCount;
×
561
        }
562

563
        $this->fixedCount       += $this->fixer->getFixCount();
×
564
        $this->fixedErrorCount   = ($this->fixableErrorCountFirstRun - $this->fixableErrorCount);
×
565
        $this->fixedWarningCount = ($this->fixableWarningCountFirstRun - $this->fixableWarningCount);
×
566

567
    }//end process()
568

569

570
    /**
571
     * Tokenizes the file and prepares it for the test run.
572
     *
573
     * @return void
574
     */
575
    public function parse()
×
576
    {
577
        if (empty($this->tokens) === false) {
×
578
            // File has already been parsed.
579
            return;
×
580
        }
581

582
        try {
583
            $this->tokenizer = new PHP($this->content, $this->config, $this->eolChar);
×
584
            $this->tokens    = $this->tokenizer->getTokens();
×
585
        } catch (TokenizerException $e) {
×
586
            $this->ignored = true;
×
587
            $this->addWarning($e->getMessage(), null, 'Internal.Tokenizer.Exception');
×
588
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
589
                $newlines = 0;
×
590
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
591
                    $newlines = 1;
×
592
                }
593

594
                StatusWriter::write('[tokenizer error]... ', 0, $newlines);
×
595
            }
596

597
            return;
×
598
        }
599

600
        $this->numTokens = count($this->tokens);
×
601

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

616
        if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
617
            if ($this->numTokens === 0) {
×
618
                $numLines = 0;
×
619
            } else {
620
                $numLines = $this->tokens[($this->numTokens - 1)]['line'];
×
621
            }
622

623
            $newlines = 0;
×
624
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
625
                $newlines = 1;
×
626
            }
627

628
            StatusWriter::write("[$this->numTokens tokens in $numLines lines]... ", 0, $newlines);
×
629
        }
630

631
    }//end parse()
632

633

634
    /**
635
     * Returns the token stack for this file.
636
     *
637
     * @return array
638
     */
639
    public function getTokens()
×
640
    {
641
        return $this->tokens;
×
642

643
    }//end getTokens()
644

645

646
    /**
647
     * Remove vars stored in this file that are no longer required.
648
     *
649
     * @return void
650
     */
651
    public function cleanUp()
×
652
    {
653
        $this->listenerTimes = null;
×
654
        $this->content       = null;
×
655
        $this->tokens        = null;
×
656
        $this->metricTokens  = null;
×
657
        $this->tokenizer     = null;
×
658
        $this->fixer         = null;
×
659
        $this->config        = null;
×
660
        $this->ruleset       = null;
×
661

662
    }//end cleanUp()
663

664

665
    /**
666
     * Records an error against a specific token in the file.
667
     *
668
     * @param string   $error    The error message.
669
     * @param int|null $stackPtr The stack position where the error occurred.
670
     * @param string   $code     A violation code unique to the sniff message.
671
     * @param array    $data     Replacements for the error message.
672
     * @param int      $severity The severity level for this error. A value of 0
673
     *                           will be converted into the default severity level.
674
     * @param boolean  $fixable  Can the error be fixed by the sniff?
675
     *
676
     * @return boolean
677
     */
678
    public function addError(
×
679
        $error,
680
        $stackPtr,
681
        $code,
682
        $data=[],
683
        $severity=0,
684
        $fixable=false
685
    ) {
686
        if ($stackPtr === null) {
×
687
            $line   = 1;
×
688
            $column = 1;
×
689
        } else {
690
            $line   = $this->tokens[$stackPtr]['line'];
×
691
            $column = $this->tokens[$stackPtr]['column'];
×
692
        }
693

694
        return $this->addMessage(true, $error, $line, $column, $code, $data, $severity, $fixable);
×
695

696
    }//end addError()
697

698

699
    /**
700
     * Records a warning against a specific token in the file.
701
     *
702
     * @param string   $warning  The error message.
703
     * @param int|null $stackPtr The stack position where the error occurred.
704
     * @param string   $code     A violation code unique to the sniff message.
705
     * @param array    $data     Replacements for the warning message.
706
     * @param int      $severity The severity level for this warning. A value of 0
707
     *                           will be converted into the default severity level.
708
     * @param boolean  $fixable  Can the warning be fixed by the sniff?
709
     *
710
     * @return boolean
711
     */
712
    public function addWarning(
×
713
        $warning,
714
        $stackPtr,
715
        $code,
716
        $data=[],
717
        $severity=0,
718
        $fixable=false
719
    ) {
720
        if ($stackPtr === null) {
×
721
            $line   = 1;
×
722
            $column = 1;
×
723
        } else {
724
            $line   = $this->tokens[$stackPtr]['line'];
×
725
            $column = $this->tokens[$stackPtr]['column'];
×
726
        }
727

728
        return $this->addMessage(false, $warning, $line, $column, $code, $data, $severity, $fixable);
×
729

730
    }//end addWarning()
731

732

733
    /**
734
     * Records an error against a specific line in the file.
735
     *
736
     * @param string $error    The error message.
737
     * @param int    $line     The line on which the error occurred.
738
     * @param string $code     A violation code unique to the sniff message.
739
     * @param array  $data     Replacements for the error message.
740
     * @param int    $severity The severity level for this error. A value of 0
741
     *                         will be converted into the default severity level.
742
     *
743
     * @return boolean
744
     */
745
    public function addErrorOnLine(
×
746
        $error,
747
        $line,
748
        $code,
749
        $data=[],
750
        $severity=0
751
    ) {
752
        return $this->addMessage(true, $error, $line, 1, $code, $data, $severity, false);
×
753

754
    }//end addErrorOnLine()
755

756

757
    /**
758
     * Records a warning against a specific line in the file.
759
     *
760
     * @param string $warning  The error message.
761
     * @param int    $line     The line on which the warning occurred.
762
     * @param string $code     A violation code unique to the sniff message.
763
     * @param array  $data     Replacements for the warning message.
764
     * @param int    $severity The severity level for this warning. A value of 0 will
765
     *                         will be converted into the default severity level.
766
     *
767
     * @return boolean
768
     */
769
    public function addWarningOnLine(
×
770
        $warning,
771
        $line,
772
        $code,
773
        $data=[],
774
        $severity=0
775
    ) {
776
        return $this->addMessage(false, $warning, $line, 1, $code, $data, $severity, false);
×
777

778
    }//end addWarningOnLine()
779

780

781
    /**
782
     * Records a fixable error against a specific token in the file.
783
     *
784
     * Returns true if the error was recorded and should be fixed.
785
     *
786
     * @param string $error    The error message.
787
     * @param int    $stackPtr The stack position where the error occurred.
788
     * @param string $code     A violation code unique to the sniff message.
789
     * @param array  $data     Replacements for the error message.
790
     * @param int    $severity The severity level for this error. A value of 0
791
     *                         will be converted into the default severity level.
792
     *
793
     * @return boolean
794
     */
795
    public function addFixableError(
×
796
        $error,
797
        $stackPtr,
798
        $code,
799
        $data=[],
800
        $severity=0
801
    ) {
802
        $recorded = $this->addError($error, $stackPtr, $code, $data, $severity, true);
×
803
        if ($recorded === true && $this->fixer->enabled === true) {
×
804
            return true;
×
805
        }
806

807
        return false;
×
808

809
    }//end addFixableError()
810

811

812
    /**
813
     * Records a fixable warning against a specific token in the file.
814
     *
815
     * Returns true if the warning was recorded and should be fixed.
816
     *
817
     * @param string $warning  The error message.
818
     * @param int    $stackPtr The stack position where the error occurred.
819
     * @param string $code     A violation code unique to the sniff message.
820
     * @param array  $data     Replacements for the warning message.
821
     * @param int    $severity The severity level for this warning. A value of 0
822
     *                         will be converted into the default severity level.
823
     *
824
     * @return boolean
825
     */
826
    public function addFixableWarning(
×
827
        $warning,
828
        $stackPtr,
829
        $code,
830
        $data=[],
831
        $severity=0
832
    ) {
833
        $recorded = $this->addWarning($warning, $stackPtr, $code, $data, $severity, true);
×
834
        if ($recorded === true && $this->fixer->enabled === true) {
×
835
            return true;
×
836
        }
837

838
        return false;
×
839

840
    }//end addFixableWarning()
841

842

843
    /**
844
     * Adds an error to the error stack.
845
     *
846
     * @param boolean $error    Is this an error message?
847
     * @param string  $message  The text of the message.
848
     * @param int     $line     The line on which the message occurred.
849
     * @param int     $column   The column at which the message occurred.
850
     * @param string  $code     A violation code unique to the sniff message.
851
     * @param array   $data     Replacements for the message.
852
     * @param int     $severity The severity level for this message. A value of 0
853
     *                          will be converted into the default severity level.
854
     * @param boolean $fixable  Can the problem be fixed by the sniff?
855
     *
856
     * @return boolean
857
     */
858
    protected function addMessage($error, $message, $line, $column, $code, $data, $severity, $fixable)
273✔
859
    {
860
        // Check if this line is ignoring all message codes.
861
        if (isset($this->tokenizer->ignoredLines[$line]['.all']) === true) {
273✔
862
            return false;
132✔
863
        }
864

865
        // Work out which sniff generated the message.
866
        $parts = explode('.', $code);
186✔
867
        if ($parts[0] === 'Internal') {
186✔
868
            // An internal message.
869
            $listenerCode = '';
18✔
870
            if ($this->activeListener !== '') {
18✔
871
                $listenerCode = Common::getSniffCode($this->activeListener);
×
872
            }
873

874
            $sniffCode  = $code;
18✔
875
            $checkCodes = [$sniffCode];
18✔
876
        } else {
877
            if ($parts[0] !== $code) {
168✔
878
                // The full message code has been passed in.
879
                $sniffCode    = $code;
×
880
                $listenerCode = substr($sniffCode, 0, strrpos($sniffCode, '.'));
×
881
            } else {
882
                $listenerCode = Common::getSniffCode($this->activeListener);
168✔
883
                $sniffCode    = $listenerCode.'.'.$code;
168✔
884
                $parts        = explode('.', $sniffCode);
168✔
885
            }
886

887
            $checkCodes = [
112✔
888
                $sniffCode,
168✔
889
                $parts[0].'.'.$parts[1].'.'.$parts[2],
168✔
890
                $parts[0].'.'.$parts[1],
168✔
891
                $parts[0],
168✔
892
            ];
112✔
893
        }//end if
894

895
        if (isset($this->tokenizer->ignoredLines[$line]) === true) {
186✔
896
            // Check if this line is ignoring this specific message.
897
            $ignored = false;
99✔
898
            foreach ($checkCodes as $checkCode) {
99✔
899
                if (isset($this->tokenizer->ignoredLines[$line][$checkCode]) === true) {
99✔
900
                    $ignored = true;
90✔
901
                    break;
90✔
902
                }
903
            }
904

905
            // If it is ignored, make sure there is no exception in place.
906
            if ($ignored === true
99✔
907
                && isset($this->tokenizer->ignoredLines[$line]['.except']) === true
99✔
908
            ) {
909
                foreach ($checkCodes as $checkCode) {
15✔
910
                    if (isset($this->tokenizer->ignoredLines[$line]['.except'][$checkCode]) === true) {
15✔
911
                        $ignored = false;
12✔
912
                        break;
12✔
913
                    }
914
                }
915
            }
916

917
            if ($ignored === true) {
99✔
918
                return false;
90✔
919
            }
920
        }//end if
921

922
        $includeAll = true;
174✔
923
        if ($this->configCache['cache'] === false
174✔
924
            || $this->configCache['recordErrors'] === false
174✔
925
        ) {
926
            $includeAll = false;
174✔
927
        }
928

929
        // Filter out any messages for sniffs that shouldn't have run
930
        // due to the use of the --sniffs or --exclude command line argument,
931
        // but don't filter out "Internal" messages.
932
        if ($includeAll === false
174✔
933
            && (($parts[0] !== 'Internal'
174✔
934
            && empty($this->configCache['sniffs']) === false
174✔
935
            && in_array(strtolower($listenerCode), $this->configCache['sniffs'], true) === false)
168✔
936
            || (empty($this->configCache['exclude']) === false
174✔
937
            && in_array(strtolower($listenerCode), $this->configCache['exclude'], true) === true))
174✔
938
        ) {
939
            return false;
×
940
        }
941

942
        // If we know this sniff code is being ignored for this file, return early.
943
        foreach ($checkCodes as $checkCode) {
174✔
944
            if (isset($this->ignoredCodes[$checkCode]) === true) {
174✔
945
                return false;
×
946
            }
947
        }
948

949
        $oppositeType = 'warning';
174✔
950
        if ($error === false) {
174✔
951
            $oppositeType = 'error';
75✔
952
        }
953

954
        foreach ($checkCodes as $checkCode) {
174✔
955
            // Make sure this message type has not been set to the opposite message type.
956
            if (isset($this->ruleset->ruleset[$checkCode]['type']) === true
174✔
957
                && $this->ruleset->ruleset[$checkCode]['type'] === $oppositeType
174✔
958
            ) {
959
                $error = !$error;
×
960
                break;
×
961
            }
962
        }
963

964
        if ($error === true) {
174✔
965
            $configSeverity = $this->configCache['errorSeverity'];
150✔
966
            $messageCount   = &$this->errorCount;
150✔
967
            $messages       = &$this->errors;
150✔
968
        } else {
969
            $configSeverity = $this->configCache['warningSeverity'];
75✔
970
            $messageCount   = &$this->warningCount;
75✔
971
            $messages       = &$this->warnings;
75✔
972
        }
973

974
        if ($includeAll === false && $configSeverity === 0) {
174✔
975
            // Don't bother doing any processing as these messages are just going to
976
            // be hidden in the reports anyway.
977
            return false;
×
978
        }
979

980
        if ($severity === 0) {
174✔
981
            $severity = 5;
174✔
982
        }
983

984
        foreach ($checkCodes as $checkCode) {
174✔
985
            // Make sure we are interested in this severity level.
986
            if (isset($this->ruleset->ruleset[$checkCode]['severity']) === true) {
174✔
987
                $severity = $this->ruleset->ruleset[$checkCode]['severity'];
9✔
988
                break;
9✔
989
            }
990
        }
991

992
        if ($includeAll === false && $configSeverity > $severity) {
174✔
993
            return false;
9✔
994
        }
995

996
        // Make sure we are not ignoring this file.
997
        $included = null;
165✔
998
        if (trim($this->path, '\'"') === 'STDIN') {
165✔
999
            $included = true;
165✔
1000
        } else {
1001
            foreach ($checkCodes as $checkCode) {
×
1002
                $patterns = null;
×
1003

1004
                if (isset($this->configCache['includePatterns'][$checkCode]) === true) {
×
1005
                    $patterns  = $this->configCache['includePatterns'][$checkCode];
×
1006
                    $excluding = false;
×
1007
                } else if (isset($this->configCache['ignorePatterns'][$checkCode]) === true) {
×
1008
                    $patterns  = $this->configCache['ignorePatterns'][$checkCode];
×
1009
                    $excluding = true;
×
1010
                }
1011

1012
                if ($patterns === null) {
×
1013
                    continue;
×
1014
                }
1015

1016
                foreach ($patterns as $pattern => $type) {
×
1017
                    // While there is support for a type of each pattern
1018
                    // (absolute or relative) we don't actually support it here.
1019
                    $replacements = [
1020
                        '\\,' => ',',
×
1021
                        '*'   => '.*',
1022
                    ];
1023

1024
                    // We assume a / directory separator, as do the exclude rules
1025
                    // most developers write, so we need a special case for any system
1026
                    // that is different.
1027
                    if (DIRECTORY_SEPARATOR === '\\') {
×
1028
                        $replacements['/'] = '\\\\';
×
1029
                    }
1030

1031
                    $pattern = '`'.strtr($pattern, $replacements).'`i';
×
1032
                    $matched = preg_match($pattern, $this->path);
×
1033

1034
                    if ($matched === 0) {
×
1035
                        if ($excluding === false && $included === null) {
×
1036
                            // This file path is not being included.
1037
                            $included = false;
×
1038
                        }
1039

1040
                        continue;
×
1041
                    }
1042

1043
                    if ($excluding === true) {
×
1044
                        // This file path is being excluded.
1045
                        $this->ignoredCodes[$checkCode] = true;
×
1046
                        return false;
×
1047
                    }
1048

1049
                    // This file path is being included.
1050
                    $included = true;
×
1051
                    break;
×
1052
                }//end foreach
1053
            }//end foreach
1054
        }//end if
1055

1056
        if ($included === false) {
165✔
1057
            // There were include rules set, but this file
1058
            // path didn't match any of them.
1059
            return false;
×
1060
        }
1061

1062
        $messageCount++;
165✔
1063
        if ($fixable === true) {
165✔
1064
            if ($error === true) {
132✔
1065
                $this->fixableErrorCount++;
132✔
1066
            } else {
1067
                $this->fixableWarningCount++;
×
1068
            }
1069
        }
1070

1071
        if ($this->configCache['recordErrors'] === false
165✔
1072
            && $includeAll === false
165✔
1073
        ) {
1074
            return true;
×
1075
        }
1076

1077
        // See if there is a custom error message format to use.
1078
        // But don't do this if we are replaying errors because replayed
1079
        // errors have already used the custom format and have had their
1080
        // data replaced.
1081
        if ($this->replayingErrors === false
165✔
1082
            && isset($this->ruleset->ruleset[$sniffCode]['message']) === true
165✔
1083
        ) {
1084
            $message = $this->ruleset->ruleset[$sniffCode]['message'];
×
1085
        }
1086

1087
        if (empty($data) === false) {
165✔
1088
            $message = vsprintf($message, $data);
153✔
1089
        }
1090

1091
        if (isset($messages[$line]) === false) {
165✔
1092
            $messages[$line] = [];
165✔
1093
        }
1094

1095
        if (isset($messages[$line][$column]) === false) {
165✔
1096
            $messages[$line][$column] = [];
165✔
1097
        }
1098

1099
        $messages[$line][$column][] = [
165✔
1100
            'message'  => $message,
165✔
1101
            'source'   => $sniffCode,
165✔
1102
            'listener' => $this->activeListener,
165✔
1103
            'severity' => $severity,
165✔
1104
            'fixable'  => $fixable,
165✔
1105
        ];
110✔
1106

1107
        if (PHP_CODESNIFFER_VERBOSITY > 1
165✔
1108
            && $this->fixer->enabled === true
165✔
1109
            && $fixable === true
165✔
1110
        ) {
1111
            StatusWriter::forceWrite("E: [Line $line] $message ($sniffCode)", 1);
×
1112
        }
1113

1114
        return true;
165✔
1115

1116
    }//end addMessage()
1117

1118

1119
    /**
1120
     * Record a metric about the file being examined.
1121
     *
1122
     * @param int    $stackPtr The stack position where the metric was recorded.
1123
     * @param string $metric   The name of the metric being recorded.
1124
     * @param string $value    The value of the metric being recorded.
1125
     *
1126
     * @return boolean
1127
     */
1128
    public function recordMetric($stackPtr, $metric, $value)
×
1129
    {
1130
        if (isset($this->metrics[$metric]) === false) {
×
1131
            $this->metrics[$metric] = ['values' => [$value => 1]];
×
1132
            $this->metricTokens[$metric][$stackPtr] = true;
×
1133
        } else if (isset($this->metricTokens[$metric][$stackPtr]) === false) {
×
1134
            $this->metricTokens[$metric][$stackPtr] = true;
×
1135
            if (isset($this->metrics[$metric]['values'][$value]) === false) {
×
1136
                $this->metrics[$metric]['values'][$value] = 1;
×
1137
            } else {
1138
                $this->metrics[$metric]['values'][$value]++;
×
1139
            }
1140
        }
1141

1142
        return true;
×
1143

1144
    }//end recordMetric()
1145

1146

1147
    /**
1148
     * Returns the number of errors raised.
1149
     *
1150
     * @return int
1151
     */
1152
    public function getErrorCount()
×
1153
    {
1154
        return $this->errorCount;
×
1155

1156
    }//end getErrorCount()
1157

1158

1159
    /**
1160
     * Returns the number of warnings raised.
1161
     *
1162
     * @return int
1163
     */
1164
    public function getWarningCount()
×
1165
    {
1166
        return $this->warningCount;
×
1167

1168
    }//end getWarningCount()
1169

1170

1171
    /**
1172
     * Returns the number of fixable errors/warnings raised.
1173
     *
1174
     * @return int
1175
     */
1176
    public function getFixableCount()
×
1177
    {
1178
        return ($this->fixableErrorCount + $this->fixableWarningCount);
×
1179

1180
    }//end getFixableCount()
1181

1182

1183
    /**
1184
     * Returns the number of fixable errors raised.
1185
     *
1186
     * @return int
1187
     */
1188
    public function getFixableErrorCount()
×
1189
    {
1190
        return $this->fixableErrorCount;
×
1191

1192
    }//end getFixableErrorCount()
1193

1194

1195
    /**
1196
     * Returns the number of fixable warnings raised.
1197
     *
1198
     * @return int
1199
     */
1200
    public function getFixableWarningCount()
×
1201
    {
1202
        return $this->fixableWarningCount;
×
1203

1204
    }//end getFixableWarningCount()
1205

1206

1207
    /**
1208
     * Returns the actual number of fixed errors/warnings.
1209
     *
1210
     * @return int
1211
     */
1212
    public function getFixedCount()
×
1213
    {
1214
        return $this->fixedCount;
×
1215

1216
    }//end getFixedCount()
1217

1218

1219
    /**
1220
     * Returns the effective number of fixed errors.
1221
     *
1222
     * @return int
1223
     */
1224
    public function getFixedErrorCount()
×
1225
    {
1226
        return $this->fixedErrorCount;
×
1227

1228
    }//end getFixedErrorCount()
1229

1230

1231
    /**
1232
     * Returns the effective number of fixed warnings.
1233
     *
1234
     * @return int
1235
     */
1236
    public function getFixedWarningCount()
×
1237
    {
1238
        return $this->fixedWarningCount;
×
1239

1240
    }//end getFixedWarningCount()
1241

1242

1243
    /**
1244
     * Returns the list of ignored lines.
1245
     *
1246
     * @return array
1247
     */
1248
    public function getIgnoredLines()
×
1249
    {
1250
        return $this->tokenizer->ignoredLines;
×
1251

1252
    }//end getIgnoredLines()
1253

1254

1255
    /**
1256
     * Returns the errors raised from processing this file.
1257
     *
1258
     * @return array
1259
     */
1260
    public function getErrors()
×
1261
    {
1262
        return $this->errors;
×
1263

1264
    }//end getErrors()
1265

1266

1267
    /**
1268
     * Returns the warnings raised from processing this file.
1269
     *
1270
     * @return array
1271
     */
1272
    public function getWarnings()
×
1273
    {
1274
        return $this->warnings;
×
1275

1276
    }//end getWarnings()
1277

1278

1279
    /**
1280
     * Returns the metrics found while processing this file.
1281
     *
1282
     * @return array
1283
     */
1284
    public function getMetrics()
×
1285
    {
1286
        return $this->metrics;
×
1287

1288
    }//end getMetrics()
1289

1290

1291
    /**
1292
     * Returns the time taken processing this file for each invoked sniff.
1293
     *
1294
     * @return array
1295
     */
1296
    public function getListenerTimes()
×
1297
    {
1298
        return $this->listenerTimes;
×
1299

1300
    }//end getListenerTimes()
1301

1302

1303
    /**
1304
     * Returns the absolute filename of this file.
1305
     *
1306
     * @return string
1307
     */
1308
    public function getFilename()
×
1309
    {
1310
        return $this->path;
×
1311

1312
    }//end getFilename()
1313

1314

1315
    /**
1316
     * Returns the declaration name for classes, interfaces, traits, enums, and functions.
1317
     *
1318
     * @param int $stackPtr The position of the declaration token which
1319
     *                      declared the class, interface, trait, or function.
1320
     *
1321
     * @return string The name of the class, interface, trait, or function or an empty string
1322
     *                if the name could not be determined (live coding).
1323
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type
1324
     *                                                      T_FUNCTION, T_CLASS, T_TRAIT, T_ENUM, or T_INTERFACE.
1325
     */
1326
    public function getDeclarationName($stackPtr)
87✔
1327
    {
1328
        $tokenCode = $this->tokens[$stackPtr]['code'];
87✔
1329

1330
        if ($tokenCode !== T_FUNCTION
87✔
1331
            && $tokenCode !== T_CLASS
87✔
1332
            && $tokenCode !== T_INTERFACE
87✔
1333
            && $tokenCode !== T_TRAIT
87✔
1334
            && $tokenCode !== T_ENUM
87✔
1335
        ) {
1336
            throw new RuntimeException('Token type "'.$this->tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM');
18✔
1337
        }
1338

1339
        $stopPoint = $this->numTokens;
69✔
1340
        if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === true) {
69✔
1341
            // For functions, stop searching at the parenthesis opener.
1342
            $stopPoint = $this->tokens[$stackPtr]['parenthesis_opener'];
35✔
1343
        } else if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
34✔
1344
            // For OO tokens, stop searching at the open curly.
1345
            $stopPoint = $this->tokens[$stackPtr]['scope_opener'];
31✔
1346
        }
1347

1348
        $content = '';
69✔
1349
        for ($i = $stackPtr; $i < $stopPoint; $i++) {
69✔
1350
            if ($this->tokens[$i]['code'] === T_STRING) {
69✔
1351
                $content = $this->tokens[$i]['content'];
63✔
1352
                break;
63✔
1353
            }
1354
        }
1355

1356
        return $content;
69✔
1357

1358
    }//end getDeclarationName()
1359

1360

1361
    /**
1362
     * Returns the method parameters for the specified function token.
1363
     *
1364
     * Also supports passing in a USE token for a closure use group.
1365
     *
1366
     * Each parameter is in the following format:
1367
     *
1368
     * <code>
1369
     *   0 => array(
1370
     *         'name'                => string,        // The variable name.
1371
     *         'token'               => integer,       // The stack pointer to the variable name.
1372
     *         'content'             => string,        // The full content of the variable definition.
1373
     *         'has_attributes'      => boolean,       // Does the parameter have one or more attributes attached ?
1374
     *         'pass_by_reference'   => boolean,       // Is the variable passed by reference?
1375
     *         'reference_token'     => integer|false, // The stack pointer to the reference operator
1376
     *                                                 // or FALSE if the param is not passed by reference.
1377
     *         'variable_length'     => boolean,       // Is the param of variable length through use of `...` ?
1378
     *         'variadic_token'      => integer|false, // The stack pointer to the ... operator
1379
     *                                                 // or FALSE if the param is not variable length.
1380
     *         'type_hint'           => string,        // The type hint for the variable.
1381
     *         'type_hint_token'     => integer|false, // The stack pointer to the start of the type hint
1382
     *                                                 // or FALSE if there is no type hint.
1383
     *         'type_hint_end_token' => integer|false, // The stack pointer to the end of the type hint
1384
     *                                                 // or FALSE if there is no type hint.
1385
     *         'nullable_type'       => boolean,       // TRUE if the type is preceded by the nullability
1386
     *                                                 // operator.
1387
     *         'comma_token'         => integer|false, // The stack pointer to the comma after the param
1388
     *                                                 // or FALSE if this is the last param.
1389
     *        )
1390
     * </code>
1391
     *
1392
     * Parameters with default values have additional array indexes of:
1393
     *         'default'             => string,  // The full content of the default value.
1394
     *         'default_token'       => integer, // The stack pointer to the start of the default value.
1395
     *         'default_equal_token' => integer, // The stack pointer to the equals sign.
1396
     *
1397
     * Parameters declared using PHP 8 constructor property promotion, have these additional array indexes:
1398
     *         'property_visibility' => string,        // The property visibility as declared.
1399
     *         'visibility_token'    => integer|false, // The stack pointer to the visibility modifier token
1400
     *                                                 // or FALSE if the visibility is not explicitly declared.
1401
     *         'property_readonly'   => boolean,       // TRUE if the readonly keyword was found.
1402
     *         'readonly_token'      => integer,       // The stack pointer to the readonly modifier token.
1403
     *                                                 // This index will only be set if the property is readonly.
1404
     *
1405
     * @param int $stackPtr The position in the stack of the function token
1406
     *                      to acquire the parameters for.
1407
     *
1408
     * @return array
1409
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified $stackPtr is not of
1410
     *                                                      type T_FUNCTION, T_CLOSURE, T_USE,
1411
     *                                                      or T_FN.
1412
     */
1413
    public function getMethodParameters($stackPtr)
237✔
1414
    {
1415
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
237✔
1416
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
237✔
1417
            && $this->tokens[$stackPtr]['code'] !== T_USE
237✔
1418
            && $this->tokens[$stackPtr]['code'] !== T_FN
237✔
1419
        ) {
1420
            throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_USE or T_FN');
9✔
1421
        }
1422

1423
        if ($this->tokens[$stackPtr]['code'] === T_USE
228✔
1424
            && isset($this->tokens[$stackPtr]['parenthesis_owner']) === false
228✔
1425
        ) {
1426
            throw new RuntimeException('$stackPtr was not a valid T_USE');
12✔
1427
        }
1428

1429
        if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === false) {
216✔
1430
            // Live coding or syntax error, so no params to find.
1431
            return [];
3✔
1432
        }
1433

1434
        $opener = $this->tokens[$stackPtr]['parenthesis_opener'];
213✔
1435

1436
        if (isset($this->tokens[$opener]['parenthesis_closer']) === false) {
213✔
1437
            // Live coding or syntax error, so no params to find.
1438
            return [];
6✔
1439
        }
1440

1441
        $closer = $this->tokens[$opener]['parenthesis_closer'];
207✔
1442

1443
        $vars            = [];
207✔
1444
        $currVar         = null;
207✔
1445
        $paramStart      = ($opener + 1);
207✔
1446
        $defaultStart    = null;
207✔
1447
        $equalToken      = null;
207✔
1448
        $paramCount      = 0;
207✔
1449
        $hasAttributes   = false;
207✔
1450
        $passByReference = false;
207✔
1451
        $referenceToken  = false;
207✔
1452
        $variableLength  = false;
207✔
1453
        $variadicToken   = false;
207✔
1454
        $typeHint        = '';
207✔
1455
        $typeHintToken   = false;
207✔
1456
        $typeHintEndToken = false;
207✔
1457
        $nullableType     = false;
207✔
1458
        $visibilityToken  = null;
207✔
1459
        $readonlyToken    = null;
207✔
1460

1461
        for ($i = $paramStart; $i <= $closer; $i++) {
207✔
1462
            // Check to see if this token has a parenthesis or bracket opener. If it does
1463
            // it's likely to be an array which might have arguments in it. This
1464
            // could cause problems in our parsing below, so lets just skip to the
1465
            // end of it.
1466
            if ($this->tokens[$i]['code'] !== T_TYPE_OPEN_PARENTHESIS
207✔
1467
                && isset($this->tokens[$i]['parenthesis_opener']) === true
207✔
1468
            ) {
1469
                // Don't do this if it's the close parenthesis for the method.
1470
                if ($i !== $this->tokens[$i]['parenthesis_closer']) {
207✔
1471
                    $i = $this->tokens[$i]['parenthesis_closer'];
9✔
1472
                    continue;
9✔
1473
                }
1474
            }
1475

1476
            if (isset($this->tokens[$i]['bracket_opener']) === true) {
207✔
1477
                if ($i !== $this->tokens[$i]['bracket_closer']) {
3✔
1478
                    $i = $this->tokens[$i]['bracket_closer'];
3✔
1479
                    continue;
3✔
1480
                }
1481
            }
1482

1483
            switch ($this->tokens[$i]['code']) {
207✔
1484
            case T_ATTRIBUTE:
207✔
1485
                $hasAttributes = true;
6✔
1486

1487
                // Skip to the end of the attribute.
1488
                $i = $this->tokens[$i]['attribute_closer'];
6✔
1489
                break;
6✔
1490
            case T_BITWISE_AND:
207✔
1491
                if ($defaultStart === null) {
54✔
1492
                    $passByReference = true;
51✔
1493
                    $referenceToken  = $i;
51✔
1494
                }
1495
                break;
54✔
1496
            case T_VARIABLE:
207✔
1497
                $currVar = $i;
198✔
1498
                break;
198✔
1499
            case T_ELLIPSIS:
207✔
1500
                $variableLength = true;
51✔
1501
                $variadicToken  = $i;
51✔
1502
                break;
51✔
1503
            case T_CALLABLE:
207✔
1504
                if ($typeHintToken === false) {
12✔
1505
                    $typeHintToken = $i;
9✔
1506
                }
1507

1508
                $typeHint        .= $this->tokens[$i]['content'];
12✔
1509
                $typeHintEndToken = $i;
12✔
1510
                break;
12✔
1511
            case T_SELF:
207✔
1512
            case T_PARENT:
207✔
1513
            case T_STATIC:
207✔
1514
                // Self and parent are valid, static invalid, but was probably intended as type hint.
1515
                if (isset($defaultStart) === false) {
18✔
1516
                    if ($typeHintToken === false) {
15✔
1517
                        $typeHintToken = $i;
12✔
1518
                    }
1519

1520
                    $typeHint        .= $this->tokens[$i]['content'];
15✔
1521
                    $typeHintEndToken = $i;
15✔
1522
                }
1523
                break;
18✔
1524
            case T_STRING:
207✔
1525
            case T_NAME_QUALIFIED:
207✔
1526
            case T_NAME_FULLY_QUALIFIED:
207✔
1527
            case T_NAME_RELATIVE:
207✔
1528
                // This is an identifier name, so it may be a type declaration, but it could
1529
                // also be a constant used as a default value.
1530
                $prevComma = false;
138✔
1531
                for ($t = $i; $t >= $opener; $t--) {
138✔
1532
                    if ($this->tokens[$t]['code'] === T_COMMA) {
138✔
1533
                        $prevComma = $t;
60✔
1534
                        break;
60✔
1535
                    }
1536
                }
1537

1538
                if ($prevComma !== false) {
138✔
1539
                    $nextEquals = false;
60✔
1540
                    for ($t = $prevComma; $t < $i; $t++) {
60✔
1541
                        if ($this->tokens[$t]['code'] === T_EQUAL) {
60✔
1542
                            $nextEquals = $t;
9✔
1543
                            break;
9✔
1544
                        }
1545
                    }
1546

1547
                    if ($nextEquals !== false) {
60✔
1548
                        break;
9✔
1549
                    }
1550
                }
1551

1552
                if ($defaultStart === null) {
135✔
1553
                    if ($typeHintToken === false) {
132✔
1554
                        $typeHintToken = $i;
117✔
1555
                    }
1556

1557
                    $typeHint        .= $this->tokens[$i]['content'];
132✔
1558
                    $typeHintEndToken = $i;
132✔
1559
                }
1560
                break;
135✔
1561
            case T_NAMESPACE:
207✔
1562
            case T_NS_SEPARATOR:
207✔
1563
            case T_TYPE_UNION:
207✔
1564
            case T_TYPE_INTERSECTION:
207✔
1565
            case T_TYPE_OPEN_PARENTHESIS:
207✔
1566
            case T_TYPE_CLOSE_PARENTHESIS:
207✔
1567
            case T_FALSE:
207✔
1568
            case T_TRUE:
207✔
1569
            case T_NULL:
207✔
1570
                // Part of a type hint or default value.
1571
                if ($defaultStart === null) {
96✔
1572
                    if ($typeHintToken === false) {
87✔
1573
                        $typeHintToken = $i;
27✔
1574
                    }
1575

1576
                    $typeHint        .= $this->tokens[$i]['content'];
87✔
1577
                    $typeHintEndToken = $i;
87✔
1578
                }
1579
                break;
96✔
1580
            case T_NULLABLE:
207✔
1581
                if ($defaultStart === null) {
63✔
1582
                    $nullableType     = true;
63✔
1583
                    $typeHint        .= $this->tokens[$i]['content'];
63✔
1584
                    $typeHintEndToken = $i;
63✔
1585
                }
1586
                break;
63✔
1587
            case T_PUBLIC:
207✔
1588
            case T_PROTECTED:
207✔
1589
            case T_PRIVATE:
207✔
1590
                if ($defaultStart === null) {
24✔
1591
                    $visibilityToken = $i;
24✔
1592
                }
1593
                break;
24✔
1594
            case T_READONLY:
207✔
1595
                if ($defaultStart === null) {
9✔
1596
                    $readonlyToken = $i;
9✔
1597
                }
1598
                break;
9✔
1599
            case T_CLOSE_PARENTHESIS:
207✔
1600
            case T_COMMA:
192✔
1601
                // If it's null, then there must be no parameters for this
1602
                // method.
1603
                if ($currVar === null) {
207✔
1604
                    continue 2;
33✔
1605
                }
1606

1607
                $vars[$paramCount]            = [];
198✔
1608
                $vars[$paramCount]['token']   = $currVar;
198✔
1609
                $vars[$paramCount]['name']    = $this->tokens[$currVar]['content'];
198✔
1610
                $vars[$paramCount]['content'] = trim($this->getTokensAsString($paramStart, ($i - $paramStart)));
198✔
1611

1612
                if ($defaultStart !== null) {
198✔
1613
                    $vars[$paramCount]['default']       = trim($this->getTokensAsString($defaultStart, ($i - $defaultStart)));
66✔
1614
                    $vars[$paramCount]['default_token'] = $defaultStart;
66✔
1615
                    $vars[$paramCount]['default_equal_token'] = $equalToken;
66✔
1616
                }
1617

1618
                $vars[$paramCount]['has_attributes']      = $hasAttributes;
198✔
1619
                $vars[$paramCount]['pass_by_reference']   = $passByReference;
198✔
1620
                $vars[$paramCount]['reference_token']     = $referenceToken;
198✔
1621
                $vars[$paramCount]['variable_length']     = $variableLength;
198✔
1622
                $vars[$paramCount]['variadic_token']      = $variadicToken;
198✔
1623
                $vars[$paramCount]['type_hint']           = $typeHint;
198✔
1624
                $vars[$paramCount]['type_hint_token']     = $typeHintToken;
198✔
1625
                $vars[$paramCount]['type_hint_end_token'] = $typeHintEndToken;
198✔
1626
                $vars[$paramCount]['nullable_type']       = $nullableType;
198✔
1627

1628
                if ($visibilityToken !== null || $readonlyToken !== null) {
198✔
1629
                    $vars[$paramCount]['property_visibility'] = 'public';
27✔
1630
                    $vars[$paramCount]['visibility_token']    = false;
27✔
1631
                    $vars[$paramCount]['property_readonly']   = false;
27✔
1632

1633
                    if ($visibilityToken !== null) {
27✔
1634
                        $vars[$paramCount]['property_visibility'] = $this->tokens[$visibilityToken]['content'];
24✔
1635
                        $vars[$paramCount]['visibility_token']    = $visibilityToken;
24✔
1636
                    }
1637

1638
                    if ($readonlyToken !== null) {
27✔
1639
                        $vars[$paramCount]['property_readonly'] = true;
9✔
1640
                        $vars[$paramCount]['readonly_token']    = $readonlyToken;
9✔
1641
                    }
1642
                }
1643

1644
                if ($this->tokens[$i]['code'] === T_COMMA) {
198✔
1645
                    $vars[$paramCount]['comma_token'] = $i;
99✔
1646
                } else {
1647
                    $vars[$paramCount]['comma_token'] = false;
174✔
1648
                }
1649

1650
                // Reset the vars, as we are about to process the next parameter.
1651
                $currVar          = null;
198✔
1652
                $paramStart       = ($i + 1);
198✔
1653
                $defaultStart     = null;
198✔
1654
                $equalToken       = null;
198✔
1655
                $hasAttributes    = false;
198✔
1656
                $passByReference  = false;
198✔
1657
                $referenceToken   = false;
198✔
1658
                $variableLength   = false;
198✔
1659
                $variadicToken    = false;
198✔
1660
                $typeHint         = '';
198✔
1661
                $typeHintToken    = false;
198✔
1662
                $typeHintEndToken = false;
198✔
1663
                $nullableType     = false;
198✔
1664
                $visibilityToken  = null;
198✔
1665
                $readonlyToken    = null;
198✔
1666

1667
                $paramCount++;
198✔
1668
                break;
198✔
1669
            case T_EQUAL:
192✔
1670
                $defaultStart = $this->findNext(Tokens::EMPTY_TOKENS, ($i + 1), null, true);
66✔
1671
                $equalToken   = $i;
66✔
1672
                break;
66✔
1673
            }//end switch
1674
        }//end for
1675

1676
        return $vars;
207✔
1677

1678
    }//end getMethodParameters()
1679

1680

1681
    /**
1682
     * Returns the visibility and implementation properties of a method.
1683
     *
1684
     * The format of the return value is:
1685
     * <code>
1686
     *   array(
1687
     *    'scope'                 => string,        // Public, private, or protected
1688
     *    'scope_specified'       => boolean,       // TRUE if the scope keyword was found.
1689
     *    'return_type'           => string,        // The return type of the method.
1690
     *    'return_type_token'     => integer|false, // The stack pointer to the start of the return type
1691
     *                                              // or FALSE if there is no return type.
1692
     *    'return_type_end_token' => integer|false, // The stack pointer to the end of the return type
1693
     *                                              // or FALSE if there is no return type.
1694
     *    'nullable_return_type'  => boolean,       // TRUE if the return type is preceded by the
1695
     *                                              // nullability operator.
1696
     *    'is_abstract'           => boolean,       // TRUE if the abstract keyword was found.
1697
     *    'is_final'              => boolean,       // TRUE if the final keyword was found.
1698
     *    'is_static'             => boolean,       // TRUE if the static keyword was found.
1699
     *    'has_body'              => boolean,       // TRUE if the method has a body
1700
     *   );
1701
     * </code>
1702
     *
1703
     * @param int $stackPtr The position in the stack of the function token to
1704
     *                      acquire the properties for.
1705
     *
1706
     * @return array
1707
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1708
     *                                                      T_FUNCTION, T_CLOSURE, or T_FN token.
1709
     */
1710
    public function getMethodProperties($stackPtr)
177✔
1711
    {
1712
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
177✔
1713
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
177✔
1714
            && $this->tokens[$stackPtr]['code'] !== T_FN
177✔
1715
        ) {
1716
            throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_FN');
9✔
1717
        }
1718

1719
        if ($this->tokens[$stackPtr]['code'] === T_FUNCTION) {
168✔
1720
            $valid = [
82✔
1721
                T_PUBLIC     => T_PUBLIC,
123✔
1722
                T_PRIVATE    => T_PRIVATE,
123✔
1723
                T_PROTECTED  => T_PROTECTED,
123✔
1724
                T_STATIC     => T_STATIC,
123✔
1725
                T_FINAL      => T_FINAL,
123✔
1726
                T_ABSTRACT   => T_ABSTRACT,
123✔
1727
                T_WHITESPACE => T_WHITESPACE,
123✔
1728
                T_COMMENT    => T_COMMENT,
123✔
1729
            ];
82✔
1730
        } else {
1731
            $valid = [
30✔
1732
                T_STATIC     => T_STATIC,
45✔
1733
                T_WHITESPACE => T_WHITESPACE,
45✔
1734
                T_COMMENT    => T_COMMENT,
45✔
1735
            ];
30✔
1736
        }
1737

1738
        $scope          = 'public';
168✔
1739
        $scopeSpecified = false;
168✔
1740
        $isAbstract     = false;
168✔
1741
        $isFinal        = false;
168✔
1742
        $isStatic       = false;
168✔
1743

1744
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
168✔
1745
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
168✔
1746
                break;
165✔
1747
            }
1748

1749
            switch ($this->tokens[$i]['code']) {
165✔
1750
            case T_PUBLIC:
165✔
1751
                $scope          = 'public';
18✔
1752
                $scopeSpecified = true;
18✔
1753
                break;
18✔
1754
            case T_PRIVATE:
165✔
1755
                $scope          = 'private';
9✔
1756
                $scopeSpecified = true;
9✔
1757
                break;
9✔
1758
            case T_PROTECTED:
165✔
1759
                $scope          = 'protected';
9✔
1760
                $scopeSpecified = true;
9✔
1761
                break;
9✔
1762
            case T_ABSTRACT:
165✔
1763
                $isAbstract = true;
9✔
1764
                break;
9✔
1765
            case T_FINAL:
165✔
1766
                $isFinal = true;
3✔
1767
                break;
3✔
1768
            case T_STATIC:
165✔
1769
                $isStatic = true;
6✔
1770
                break;
6✔
1771
            }//end switch
1772
        }//end for
1773

1774
        $returnType         = '';
168✔
1775
        $returnTypeToken    = false;
168✔
1776
        $returnTypeEndToken = false;
168✔
1777
        $nullableReturnType = false;
168✔
1778
        $hasBody            = true;
168✔
1779

1780
        if (isset($this->tokens[$stackPtr]['parenthesis_closer']) === true) {
168✔
1781
            $scopeOpener = null;
168✔
1782
            if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
168✔
1783
                $scopeOpener = $this->tokens[$stackPtr]['scope_opener'];
153✔
1784
            }
1785

1786
            $valid  = Tokens::NAME_TOKENS;
168✔
1787
            $valid += [
112✔
1788
                T_CALLABLE               => T_CALLABLE,
168✔
1789
                T_SELF                   => T_SELF,
168✔
1790
                T_PARENT                 => T_PARENT,
168✔
1791
                T_STATIC                 => T_STATIC,
168✔
1792
                T_FALSE                  => T_FALSE,
168✔
1793
                T_TRUE                   => T_TRUE,
168✔
1794
                T_NULL                   => T_NULL,
168✔
1795
                T_TYPE_UNION             => T_TYPE_UNION,
168✔
1796
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
168✔
1797
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
168✔
1798
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
168✔
1799
            ];
112✔
1800

1801
            for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) {
168✔
1802
                if (($scopeOpener === null && $this->tokens[$i]['code'] === T_SEMICOLON)
168✔
1803
                    || ($scopeOpener !== null && $i === $scopeOpener)
168✔
1804
                ) {
1805
                    // End of function definition.
1806
                    break;
168✔
1807
                }
1808

1809
                // Skip over closure use statements.
1810
                if ($this->tokens[$i]['code'] === T_USE) {
168✔
1811
                    if (isset($this->tokens[$i]['parenthesis_closer']) === false) {
15✔
1812
                        // Live coding/parse error, stop parsing.
1813
                        break;
×
1814
                    }
1815

1816
                    $i = $this->tokens[$i]['parenthesis_closer'];
15✔
1817
                    continue;
15✔
1818
                }
1819

1820
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
168✔
1821
                    $nullableReturnType = true;
39✔
1822
                }
1823

1824
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
168✔
1825
                    if ($returnTypeToken === false) {
144✔
1826
                        $returnTypeToken = $i;
144✔
1827
                    }
1828

1829
                    $returnType        .= $this->tokens[$i]['content'];
144✔
1830
                    $returnTypeEndToken = $i;
144✔
1831
                }
1832
            }//end for
1833

1834
            if ($this->tokens[$stackPtr]['code'] === T_FN) {
168✔
1835
                $bodyToken = T_FN_ARROW;
18✔
1836
            } else {
1837
                $bodyToken = T_OPEN_CURLY_BRACKET;
150✔
1838
            }
1839

1840
            $end     = $this->findNext([$bodyToken, T_SEMICOLON], $this->tokens[$stackPtr]['parenthesis_closer']);
168✔
1841
            $hasBody = $this->tokens[$end]['code'] === $bodyToken;
168✔
1842
        }//end if
1843

1844
        if ($returnType !== '' && $nullableReturnType === true) {
168✔
1845
            $returnType = '?'.$returnType;
39✔
1846
        }
1847

1848
        return [
112✔
1849
            'scope'                 => $scope,
168✔
1850
            'scope_specified'       => $scopeSpecified,
168✔
1851
            'return_type'           => $returnType,
168✔
1852
            'return_type_token'     => $returnTypeToken,
168✔
1853
            'return_type_end_token' => $returnTypeEndToken,
168✔
1854
            'nullable_return_type'  => $nullableReturnType,
168✔
1855
            'is_abstract'           => $isAbstract,
168✔
1856
            'is_final'              => $isFinal,
168✔
1857
            'is_static'             => $isStatic,
168✔
1858
            'has_body'              => $hasBody,
168✔
1859
        ];
112✔
1860

1861
    }//end getMethodProperties()
1862

1863

1864
    /**
1865
     * Returns the visibility and implementation properties of a class member var.
1866
     *
1867
     * The format of the return value is:
1868
     *
1869
     * <code>
1870
     *   array(
1871
     *    'scope'           => string,        // Public, private, or protected.
1872
     *    'scope_specified' => boolean,       // TRUE if the scope was explicitly specified.
1873
     *    'is_static'       => boolean,       // TRUE if the static keyword was found.
1874
     *    'is_readonly'     => boolean,       // TRUE if the readonly keyword was found.
1875
     *    'is_final'        => boolean,       // TRUE if the final keyword was found.
1876
     *    'type'            => string,        // The type of the var (empty if no type specified).
1877
     *    'type_token'      => integer|false, // The stack pointer to the start of the type
1878
     *                                        // or FALSE if there is no type.
1879
     *    'type_end_token'  => integer|false, // The stack pointer to the end of the type
1880
     *                                        // or FALSE if there is no type.
1881
     *    'nullable_type'   => boolean,       // TRUE if the type is preceded by the nullability
1882
     *                                        // operator.
1883
     *   );
1884
     * </code>
1885
     *
1886
     * @param int $stackPtr The position in the stack of the T_VARIABLE token to
1887
     *                      acquire the properties for.
1888
     *
1889
     * @return array
1890
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1891
     *                                                      T_VARIABLE token, or if the position is not
1892
     *                                                      a class member variable.
1893
     */
1894
    public function getMemberProperties($stackPtr)
300✔
1895
    {
1896
        if ($this->tokens[$stackPtr]['code'] !== T_VARIABLE) {
300✔
1897
            throw new RuntimeException('$stackPtr must be of type T_VARIABLE');
3✔
1898
        }
1899

1900
        $conditions = $this->tokens[$stackPtr]['conditions'];
297✔
1901
        $conditions = array_keys($conditions);
297✔
1902
        $ptr        = array_pop($conditions);
297✔
1903
        if (isset($this->tokens[$ptr]) === false
297✔
1904
            || isset(Tokens::OO_SCOPE_TOKENS[$this->tokens[$ptr]['code']]) === false
294✔
1905
            || $this->tokens[$ptr]['code'] === T_ENUM
297✔
1906
        ) {
1907
            throw new RuntimeException('$stackPtr is not a class member var');
15✔
1908
        }
1909

1910
        // Make sure it's not a method parameter.
1911
        if (empty($this->tokens[$stackPtr]['nested_parenthesis']) === false) {
282✔
1912
            $parenthesis = array_keys($this->tokens[$stackPtr]['nested_parenthesis']);
15✔
1913
            $deepestOpen = array_pop($parenthesis);
15✔
1914
            if ($deepestOpen > $ptr
15✔
1915
                && isset($this->tokens[$deepestOpen]['parenthesis_owner']) === true
15✔
1916
                && $this->tokens[$this->tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION
15✔
1917
            ) {
1918
                throw new RuntimeException('$stackPtr is not a class member var');
9✔
1919
            }
1920
        }
1921

1922
        $valid = [
182✔
1923
            T_PUBLIC    => T_PUBLIC,
273✔
1924
            T_PRIVATE   => T_PRIVATE,
273✔
1925
            T_PROTECTED => T_PROTECTED,
273✔
1926
            T_STATIC    => T_STATIC,
273✔
1927
            T_VAR       => T_VAR,
273✔
1928
            T_READONLY  => T_READONLY,
273✔
1929
            T_FINAL     => T_FINAL,
273✔
1930
        ];
182✔
1931

1932
        $valid += Tokens::EMPTY_TOKENS;
273✔
1933

1934
        $scope          = 'public';
273✔
1935
        $scopeSpecified = false;
273✔
1936
        $isStatic       = false;
273✔
1937
        $isReadonly     = false;
273✔
1938
        $isFinal        = false;
273✔
1939

1940
        $startOfStatement = $this->findPrevious(
273✔
1941
            [
182✔
1942
                T_SEMICOLON,
273✔
1943
                T_OPEN_CURLY_BRACKET,
273✔
1944
                T_CLOSE_CURLY_BRACKET,
273✔
1945
                T_ATTRIBUTE_END,
273✔
1946
            ],
182✔
1947
            ($stackPtr - 1)
273✔
1948
        );
182✔
1949

1950
        for ($i = ($startOfStatement + 1); $i < $stackPtr; $i++) {
273✔
1951
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
273✔
1952
                break;
207✔
1953
            }
1954

1955
            switch ($this->tokens[$i]['code']) {
273✔
1956
            case T_PUBLIC:
273✔
1957
                $scope          = 'public';
129✔
1958
                $scopeSpecified = true;
129✔
1959
                break;
129✔
1960
            case T_PRIVATE:
273✔
1961
                $scope          = 'private';
57✔
1962
                $scopeSpecified = true;
57✔
1963
                break;
57✔
1964
            case T_PROTECTED:
273✔
1965
                $scope          = 'protected';
45✔
1966
                $scopeSpecified = true;
45✔
1967
                break;
45✔
1968
            case T_STATIC:
273✔
1969
                $isStatic = true;
66✔
1970
                break;
66✔
1971
            case T_READONLY:
273✔
1972
                $isReadonly = true;
36✔
1973
                break;
36✔
1974
            case T_FINAL:
273✔
1975
                $isFinal = true;
27✔
1976
                break;
27✔
1977
            }//end switch
1978
        }//end for
1979

1980
        $type         = '';
273✔
1981
        $typeToken    = false;
273✔
1982
        $typeEndToken = false;
273✔
1983
        $nullableType = false;
273✔
1984

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

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

2007
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
183✔
2008
                    $nullableType = true;
51✔
2009
                }
2010

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

2017
                    $type .= $this->tokens[$i]['content'];
183✔
2018
                }
2019
            }
2020

2021
            if ($type !== '' && $nullableType === true) {
207✔
2022
                $type = '?'.$type;
51✔
2023
            }
2024
        }//end if
2025

2026
        return [
182✔
2027
            'scope'           => $scope,
273✔
2028
            'scope_specified' => $scopeSpecified,
273✔
2029
            'is_static'       => $isStatic,
273✔
2030
            'is_readonly'     => $isReadonly,
273✔
2031
            'is_final'        => $isFinal,
273✔
2032
            'type'            => $type,
273✔
2033
            'type_token'      => $typeToken,
273✔
2034
            'type_end_token'  => $typeEndToken,
273✔
2035
            'nullable_type'   => $nullableType,
273✔
2036
        ];
182✔
2037

2038
    }//end getMemberProperties()
2039

2040

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

2066
        $valid = [
22✔
2067
            T_FINAL      => T_FINAL,
33✔
2068
            T_ABSTRACT   => T_ABSTRACT,
33✔
2069
            T_READONLY   => T_READONLY,
33✔
2070
            T_WHITESPACE => T_WHITESPACE,
33✔
2071
            T_COMMENT    => T_COMMENT,
33✔
2072
        ];
22✔
2073

2074
        $isAbstract = false;
33✔
2075
        $isFinal    = false;
33✔
2076
        $isReadonly = false;
33✔
2077

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

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

2088
            case T_FINAL:
33✔
2089
                $isFinal = true;
12✔
2090
                break;
12✔
2091

2092
            case T_READONLY:
33✔
2093
                $isReadonly = true;
15✔
2094
                break;
15✔
2095
            }
2096
        }//end for
2097

2098
        return [
22✔
2099
            'is_abstract' => $isAbstract,
33✔
2100
            'is_final'    => $isFinal,
33✔
2101
            'is_readonly' => $isReadonly,
33✔
2102
        ];
22✔
2103

2104
    }//end getClassProperties()
2105

2106

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

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

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

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

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

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

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

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

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

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

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

2213
        return false;
36✔
2214

2215
    }//end isReference()
2216

2217

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

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

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

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

2256
        return $str;
69✔
2257

2258
    }//end getTokensAsString()
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
        $start,
2290
        $end=null,
2291
        $exclude=false,
2292
        $value=null,
2293
        $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
                } else if ($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
                } else if (isset($this->tokens[$i]['bracket_opener']) === true
×
2324
                    && $i === $this->tokens[$i]['bracket_closer']
×
2325
                ) {
2326
                    $i = $this->tokens[$i]['bracket_opener'];
×
2327
                } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
×
2328
                    && $i === $this->tokens[$i]['parenthesis_closer']
×
2329
                ) {
2330
                    $i = $this->tokens[$i]['parenthesis_opener'];
×
2331
                } else if ($this->tokens[$i]['code'] === T_SEMICOLON) {
×
2332
                    break;
×
2333
                }
2334
            }
2335
        }//end for
2336

2337
        return false;
×
2338

2339
    }//end findPrevious()
2340

2341

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

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

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

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

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

2404
        return false;
×
2405

2406
    }//end findNext()
2407

2408

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

2424
        $endTokens = [
140✔
2425
            T_CLOSE_TAG    => true,
210✔
2426
            T_COLON        => true,
210✔
2427
            T_COMMA        => true,
210✔
2428
            T_DOUBLE_ARROW => true,
210✔
2429
            T_MATCH_ARROW  => true,
210✔
2430
            T_SEMICOLON    => true,
210✔
2431
        ];
140✔
2432

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

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

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

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

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

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

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

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

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

2511
                        return $next;
12✔
2512
                    }
2513

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

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

2528
                    return $next;
33✔
2529
                }//end if
2530
            }//end if
2531
        }//end if
2532

2533
        $lastNotEmpty = $start;
180✔
2534

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

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

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

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

2590
            if (isset(Tokens::EMPTY_TOKENS[$this->tokens[$i]['code']]) === false) {
177✔
2591
                $lastNotEmpty = $i;
177✔
2592
            }
2593
        }//end for
2594

2595
        return 0;
×
2596

2597
    }//end findStartOfStatement()
2598

2599

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

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

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

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

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

2671
                return $i;
48✔
2672
            }
2673

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

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

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

2705
            if (isset(Tokens::EMPTY_TOKENS[$this->tokens[$i]['code']]) === false) {
48✔
2706
                $lastNotEmpty = $i;
48✔
2707
            }
2708
        }//end for
2709

2710
        return ($this->numTokens - 1);
3✔
2711

2712
    }//end findEndOfStatement()
2713

2714

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

2741
        $foundToken = false;
×
2742

2743
        for ($i = $start; $i >= 0; $i--) {
×
2744
            if ($this->tokens[$i]['line'] < $this->tokens[$start]['line']) {
×
2745
                break;
×
2746
            }
2747

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

2763
            if ($found === true) {
×
2764
                if ($value === null) {
×
2765
                    $foundToken = $i;
×
2766
                } else if ($this->tokens[$i]['content'] === $value) {
×
2767
                    $foundToken = $i;
×
2768
                }
2769
            }
2770
        }//end for
2771

2772
        return $foundToken;
×
2773

2774
    }//end findFirstOnLine()
2775

2776

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

2792
        // Make sure the token has conditions.
2793
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
18✔
2794
            return false;
×
2795
        }
2796

2797
        $types      = (array) $types;
18✔
2798
        $conditions = $this->tokens[$stackPtr]['conditions'];
18✔
2799

2800
        foreach ($types as $type) {
18✔
2801
            if (in_array($type, $conditions, true) === true) {
18✔
2802
                // We found a token with the required type.
2803
                return true;
15✔
2804
            }
2805
        }
2806

2807
        return false;
18✔
2808

2809
    }//end hasCondition()
2810

2811

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

2833
        // Make sure the token has conditions.
2834
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
27✔
2835
            return false;
×
2836
        }
2837

2838
        $conditions = $this->tokens[$stackPtr]['conditions'];
27✔
2839
        if ($first === false) {
27✔
2840
            $conditions = array_reverse($conditions, true);
12✔
2841
        }
2842

2843
        foreach ($conditions as $token => $condition) {
27✔
2844
            if ($condition === $type) {
27✔
2845
                return $token;
24✔
2846
            }
2847
        }
2848

2849
        return false;
27✔
2850

2851
    }//end getCondition()
2852

2853

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

2871
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
51✔
2872
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
51✔
2873
            && $this->tokens[$stackPtr]['code'] !== T_INTERFACE
51✔
2874
        ) {
2875
            return false;
3✔
2876
        }
2877

2878
        if (isset($this->tokens[$stackPtr]['scope_opener']) === false) {
48✔
2879
            return false;
3✔
2880
        }
2881

2882
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
45✔
2883
        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex);
45✔
2884
        if ($extendsIndex === false) {
45✔
2885
            return false;
9✔
2886
        }
2887

2888
        $find   = Tokens::NAME_TOKENS;
36✔
2889
        $find[] = T_WHITESPACE;
36✔
2890

2891
        $end  = $this->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true);
36✔
2892
        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
36✔
2893
        $name = trim($name);
36✔
2894

2895
        if ($name === '') {
36✔
2896
            return false;
3✔
2897
        }
2898

2899
        return $name;
33✔
2900

2901
    }//end findExtendedClassName()
2902

2903

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

2920
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
48✔
2921
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
48✔
2922
            && $this->tokens[$stackPtr]['code'] !== T_ENUM
48✔
2923
        ) {
2924
            return false;
6✔
2925
        }
2926

2927
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
42✔
2928
            return false;
3✔
2929
        }
2930

2931
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
39✔
2932
        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
39✔
2933
        if ($implementsIndex === false) {
39✔
2934
            return false;
6✔
2935
        }
2936

2937
        $find   = Tokens::NAME_TOKENS;
33✔
2938
        $find[] = T_WHITESPACE;
33✔
2939
        $find[] = T_COMMA;
33✔
2940

2941
        $end  = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
33✔
2942
        $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
33✔
2943
        $name = trim($name);
33✔
2944

2945
        if ($name === '') {
33✔
2946
            return false;
3✔
2947
        } else {
2948
            $names = explode(',', $name);
30✔
2949
            $names = array_map('trim', $names);
30✔
2950
            return $names;
30✔
2951
        }
2952

2953
    }//end findImplementedInterfaceNames()
2954

2955

2956
}//end class
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc