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

PHPCSStandards / PHP_CodeSniffer / 14516416464

17 Apr 2025 01:09PM UTC coverage: 77.945% (+0.3%) from 77.666%
14516416464

push

github

web-flow
Merge pull request #1010 from PHPCSStandards/phpcs-4.0/feature/sq-1612-stdout-vs-stderr

(Nearly) All status, debug, and progress output is now sent to STDERR instead of STDOUT

63 of 457 new or added lines in 18 files covered. (13.79%)

1 existing line in 1 file now uncovered.

19455 of 24960 relevant lines covered (77.94%)

78.64 hits per line

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

70.7
/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 total number of errors and warnings that can be fixed.
157
     *
158
     * @var integer
159
     */
160
    protected $fixableCount = 0;
161

162
    /**
163
     * The total number of errors and warnings that were fixed.
164
     *
165
     * @var integer
166
     */
167
    protected $fixedCount = 0;
168

169
    /**
170
     * TRUE if errors are being replayed from the cache.
171
     *
172
     * @var boolean
173
     */
174
    protected $replayingErrors = false;
175

176
    /**
177
     * An array of sniffs that are being ignored.
178
     *
179
     * @var array
180
     */
181
    protected $ignoredListeners = [];
182

183
    /**
184
     * An array of message codes that are being ignored.
185
     *
186
     * @var array
187
     */
188
    protected $ignoredCodes = [];
189

190
    /**
191
     * An array of sniffs listening to this file's processing.
192
     *
193
     * @var \PHP_CodeSniffer\Sniffs\Sniff[]
194
     */
195
    protected $listeners = [];
196

197
    /**
198
     * The class name of the sniff currently processing the file.
199
     *
200
     * @var string
201
     */
202
    protected $activeListener = '';
203

204
    /**
205
     * An array of sniffs being processed and how long they took.
206
     *
207
     * @var array
208
     * @see getListenerTimes()
209
     */
210
    protected $listenerTimes = [];
211

212
    /**
213
     * A cache of often used config settings to improve performance.
214
     *
215
     * Storing them here saves 10k+ calls to __get() in the Config class.
216
     *
217
     * @var array
218
     */
219
    protected $configCache = [];
220

221

222
    /**
223
     * Constructs a file.
224
     *
225
     * @param string                   $path    The absolute path to the file to process.
226
     * @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run.
227
     * @param \PHP_CodeSniffer\Config  $config  The config data for the run.
228
     *
229
     * @return void
230
     */
231
    public function __construct($path, Ruleset $ruleset, Config $config)
×
232
    {
233
        $this->path    = $path;
×
234
        $this->ruleset = $ruleset;
×
235
        $this->config  = $config;
×
236
        $this->fixer   = new Fixer();
×
237

238
        $this->configCache['cache']           = $this->config->cache;
×
239
        $this->configCache['sniffs']          = array_map('strtolower', $this->config->sniffs);
×
240
        $this->configCache['exclude']         = array_map('strtolower', $this->config->exclude);
×
241
        $this->configCache['errorSeverity']   = $this->config->errorSeverity;
×
242
        $this->configCache['warningSeverity'] = $this->config->warningSeverity;
×
243
        $this->configCache['recordErrors']    = $this->config->recordErrors;
×
244
        $this->configCache['trackTime']       = $this->config->trackTime;
×
245
        $this->configCache['ignorePatterns']  = $this->ruleset->ignorePatterns;
×
246
        $this->configCache['includePatterns'] = $this->ruleset->includePatterns;
×
247

248
    }//end __construct()
249

250

251
    /**
252
     * Set the content of the file.
253
     *
254
     * Setting the content also calculates the EOL char being used.
255
     *
256
     * @param string $content The file content.
257
     *
258
     * @return void
259
     */
260
    public function setContent($content)
×
261
    {
262
        $this->content = $content;
×
263
        $this->tokens  = [];
×
264

265
        try {
266
            $this->eolChar = Common::detectLineEndings($content);
×
267
        } catch (RuntimeException $e) {
×
268
            $this->addWarningOnLine($e->getMessage(), 1, 'Internal.DetectLineEndings');
×
269
            return;
×
270
        }
271

272
    }//end setContent()
273

274

275
    /**
276
     * Reloads the content of the file.
277
     *
278
     * By default, we have no idea where our content comes from,
279
     * so we can't do anything.
280
     *
281
     * @return void
282
     */
283
    public function reloadContent()
×
284
    {
285

286
    }//end reloadContent()
×
287

288

289
    /**
290
     * Disables caching of this file.
291
     *
292
     * @return void
293
     */
294
    public function disableCaching()
×
295
    {
296
        $this->configCache['cache'] = false;
×
297

298
    }//end disableCaching()
299

300

301
    /**
302
     * Starts the stack traversal and tells listeners when tokens are found.
303
     *
304
     * @return void
305
     */
306
    public function process()
×
307
    {
308
        if ($this->ignored === true) {
×
309
            return;
×
310
        }
311

312
        $this->errors       = [];
×
313
        $this->warnings     = [];
×
314
        $this->errorCount   = 0;
×
315
        $this->warningCount = 0;
×
316
        $this->fixableCount = 0;
×
317

318
        $this->parse();
×
319

320
        // Check if tokenizer errors cause this file to be ignored.
321
        if ($this->ignored === true) {
×
322
            return;
×
323
        }
324

325
        $this->fixer->startFile($this);
×
326

327
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
NEW
328
            StatusWriter::write('*** START TOKEN PROCESSING ***', 1);
×
329
        }
330

331
        $foundCode        = false;
×
332
        $listenerIgnoreTo = [];
×
333
        $inTests          = defined('PHP_CODESNIFFER_IN_TESTS');
×
334
        $checkAnnotations = $this->config->annotations;
×
335

336
        // Foreach of the listeners that have registered to listen for this
337
        // token, get them to process it.
338
        foreach ($this->tokens as $stackPtr => $token) {
×
339
            // Check for ignored lines.
340
            if ($checkAnnotations === true
×
341
                && ($token['code'] === T_COMMENT
×
342
                || $token['code'] === T_PHPCS_IGNORE_FILE
×
343
                || $token['code'] === T_PHPCS_SET
×
344
                || $token['code'] === T_DOC_COMMENT_STRING
×
345
                || $token['code'] === T_DOC_COMMENT_TAG
×
346
                || ($inTests === true && $token['code'] === T_INLINE_HTML))
×
347
            ) {
348
                $commentText      = ltrim($this->tokens[$stackPtr]['content'], " \t/*#");
×
349
                $commentTextLower = strtolower($commentText);
×
350
                if (substr($commentTextLower, 0, 16) === 'phpcs:ignorefile'
×
351
                    || substr($commentTextLower, 0, 17) === '@phpcs:ignorefile'
×
352
                ) {
353
                    // Ignoring the whole file, just a little late.
354
                    $this->errors       = [];
×
355
                    $this->warnings     = [];
×
356
                    $this->errorCount   = 0;
×
357
                    $this->warningCount = 0;
×
358
                    $this->fixableCount = 0;
×
359
                    return;
×
360
                } else if (substr($commentTextLower, 0, 9) === 'phpcs:set'
×
361
                    || substr($commentTextLower, 0, 10) === '@phpcs:set'
×
362
                ) {
363
                    if (isset($token['sniffCode']) === true) {
×
364
                        $listenerCode = $token['sniffCode'];
×
365
                        if (isset($this->ruleset->sniffCodes[$listenerCode]) === true) {
×
366
                            $propertyCode  = $token['sniffProperty'];
×
367
                            $settings      = [
368
                                'value' => $token['sniffPropertyValue'],
×
369
                                'scope' => 'sniff',
×
370
                            ];
371
                            $listenerClass = $this->ruleset->sniffCodes[$listenerCode];
×
372
                            $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $settings);
×
373
                        }
374
                    }
375
                }//end if
376
            }//end if
377

378
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
379
                $type    = $token['type'];
×
380
                $content = Common::prepareForOutput($token['content']);
×
NEW
381
                StatusWriter::write("Process token $stackPtr: $type => $content", 2);
×
382
            }
383

384
            if ($token['code'] !== T_INLINE_HTML) {
×
385
                $foundCode = true;
×
386
            }
387

388
            if (isset($this->ruleset->tokenListeners[$token['code']]) === false) {
×
389
                continue;
×
390
            }
391

392
            foreach ($this->ruleset->tokenListeners[$token['code']] as $listenerData) {
×
393
                if (isset($this->ignoredListeners[$listenerData['class']]) === true
×
394
                    || (isset($listenerIgnoreTo[$listenerData['class']]) === true
×
395
                    && $listenerIgnoreTo[$listenerData['class']] > $stackPtr)
×
396
                ) {
397
                    // This sniff is ignoring past this token, or the whole file.
398
                    continue;
×
399
                }
400

401
                $class = $listenerData['class'];
×
402

403
                if (trim($this->path, '\'"') !== 'STDIN') {
×
404
                    // If the file path matches one of our ignore patterns, skip it.
405
                    // While there is support for a type of each pattern
406
                    // (absolute or relative) we don't actually support it here.
407
                    foreach ($listenerData['ignore'] as $pattern) {
×
408
                        // We assume a / directory separator, as do the exclude rules
409
                        // most developers write, so we need a special case for any system
410
                        // that is different.
411
                        if (DIRECTORY_SEPARATOR === '\\') {
×
412
                            $pattern = str_replace('/', '\\\\', $pattern);
×
413
                        }
414

415
                        $pattern = '`'.$pattern.'`i';
×
416
                        if (preg_match($pattern, $this->path) === 1) {
×
417
                            $this->ignoredListeners[$class] = true;
×
418
                            continue(2);
×
419
                        }
420
                    }
421

422
                    // If the file path does not match one of our include patterns, skip it.
423
                    // While there is support for a type of each pattern
424
                    // (absolute or relative) we don't actually support it here.
425
                    if (empty($listenerData['include']) === false) {
×
426
                        $included = false;
×
427
                        foreach ($listenerData['include'] as $pattern) {
×
428
                            // We assume a / directory separator, as do the exclude rules
429
                            // most developers write, so we need a special case for any system
430
                            // that is different.
431
                            if (DIRECTORY_SEPARATOR === '\\') {
×
432
                                $pattern = str_replace('/', '\\\\', $pattern);
×
433
                            }
434

435
                            $pattern = '`'.$pattern.'`i';
×
436
                            if (preg_match($pattern, $this->path) === 1) {
×
437
                                $included = true;
×
438
                                break;
×
439
                            }
440
                        }
441

442
                        if ($included === false) {
×
443
                            $this->ignoredListeners[$class] = true;
×
444
                            continue;
×
445
                        }
446
                    }//end if
447
                }//end if
448

449
                $this->activeListener = $class;
×
450

451
                if ($this->configCache['trackTime'] === true) {
×
452
                    $startTime = microtime(true);
×
453
                }
454

455
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
NEW
456
                    StatusWriter::write('Processing '.$this->activeListener.'... ', 3, 0);
×
457
                }
458

459
                $ignoreTo = $this->ruleset->sniffs[$class]->process($this, $stackPtr);
×
460
                if ($ignoreTo !== null) {
×
461
                    $listenerIgnoreTo[$this->activeListener] = $ignoreTo;
×
462
                }
463

464
                if ($this->configCache['trackTime'] === true) {
×
465
                    $timeTaken = (microtime(true) - $startTime);
×
466
                    if (isset($this->listenerTimes[$this->activeListener]) === false) {
×
467
                        $this->listenerTimes[$this->activeListener] = 0;
×
468
                    }
469

470
                    $this->listenerTimes[$this->activeListener] += $timeTaken;
×
471
                }
472

473
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
474
                    $timeTaken = round(($timeTaken), 4);
×
NEW
475
                    StatusWriter::write("DONE in $timeTaken seconds");
×
476
                }
477

478
                $this->activeListener = '';
×
479
            }//end foreach
480
        }//end foreach
481

482
        // If short open tags are off but the file being checked uses
483
        // short open tags, the whole content will be inline HTML
484
        // and nothing will be checked. So try and handle this case.
485
        // We don't show this error for STDIN because we can't be sure the content
486
        // actually came directly from the user. It could be something like
487
        // refs from a Git pre-push hook.
488
        if ($foundCode === false && $this->path !== 'STDIN') {
×
489
            $shortTags = (bool) ini_get('short_open_tag');
×
490
            if ($shortTags === false) {
×
491
                $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.';
×
492
                $this->addWarning($error, null, 'Internal.NoCodeFound');
×
493
            }
494
        }
495

496
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
NEW
497
            StatusWriter::write('*** END TOKEN PROCESSING ***', 1);
×
NEW
498
            StatusWriter::write('*** START SNIFF PROCESSING REPORT ***', 1);
×
499

500
            arsort($this->listenerTimes, SORT_NUMERIC);
×
501
            foreach ($this->listenerTimes as $listener => $timeTaken) {
×
NEW
502
                StatusWriter::write("$listener: ".round(($timeTaken), 4).' secs', 1);
×
503
            }
504

NEW
505
            StatusWriter::write('*** END SNIFF PROCESSING REPORT ***', 1);
×
506
        }
507

508
        $this->fixedCount += $this->fixer->getFixCount();
×
509

510
    }//end process()
511

512

513
    /**
514
     * Tokenizes the file and prepares it for the test run.
515
     *
516
     * @return void
517
     */
518
    public function parse()
×
519
    {
520
        if (empty($this->tokens) === false) {
×
521
            // File has already been parsed.
522
            return;
×
523
        }
524

525
        try {
526
            $this->tokenizer = new PHP($this->content, $this->config, $this->eolChar);
×
527
            $this->tokens    = $this->tokenizer->getTokens();
×
528
        } catch (TokenizerException $e) {
×
529
            $this->ignored = true;
×
530
            $this->addWarning($e->getMessage(), null, 'Internal.Tokenizer.Exception');
×
531
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
NEW
532
                $newlines = 0;
×
533
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
NEW
534
                    $newlines = 1;
×
535
                }
536

NEW
537
                StatusWriter::write('[tokenizer error]... ', 0, $newlines);
×
538
            }
539

540
            return;
×
541
        }
542

543
        $this->numTokens = count($this->tokens);
×
544

545
        // Check for mixed line endings as these can cause tokenizer errors and we
546
        // should let the user know that the results they get may be incorrect.
547
        // This is done by removing all backslashes, removing the newline char we
548
        // detected, then converting newlines chars into text. If any backslashes
549
        // are left at the end, we have additional newline chars in use.
550
        $contents = str_replace('\\', '', $this->content);
×
551
        $contents = str_replace($this->eolChar, '', $contents);
×
552
        $contents = str_replace("\n", '\n', $contents);
×
553
        $contents = str_replace("\r", '\r', $contents);
×
554
        if (strpos($contents, '\\') !== false) {
×
555
            $error = 'File has mixed line endings; this may cause incorrect results';
×
556
            $this->addWarningOnLine($error, 1, 'Internal.LineEndings.Mixed');
×
557
        }
558

559
        if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
560
            if ($this->numTokens === 0) {
×
561
                $numLines = 0;
×
562
            } else {
563
                $numLines = $this->tokens[($this->numTokens - 1)]['line'];
×
564
            }
565

NEW
566
            $newlines = 0;
×
567
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
NEW
568
                $newlines = 1;
×
569
            }
570

NEW
571
            StatusWriter::write("[$this->numTokens tokens in $numLines lines]... ", 0, $newlines);
×
572
        }
573

574
    }//end parse()
575

576

577
    /**
578
     * Returns the token stack for this file.
579
     *
580
     * @return array
581
     */
582
    public function getTokens()
×
583
    {
584
        return $this->tokens;
×
585

586
    }//end getTokens()
587

588

589
    /**
590
     * Remove vars stored in this file that are no longer required.
591
     *
592
     * @return void
593
     */
594
    public function cleanUp()
×
595
    {
596
        $this->listenerTimes = null;
×
597
        $this->content       = null;
×
598
        $this->tokens        = null;
×
599
        $this->metricTokens  = null;
×
600
        $this->tokenizer     = null;
×
601
        $this->fixer         = null;
×
602
        $this->config        = null;
×
603
        $this->ruleset       = null;
×
604

605
    }//end cleanUp()
606

607

608
    /**
609
     * Records an error against a specific token in the file.
610
     *
611
     * @param string   $error    The error message.
612
     * @param int|null $stackPtr The stack position where the error occurred.
613
     * @param string   $code     A violation code unique to the sniff message.
614
     * @param array    $data     Replacements for the error message.
615
     * @param int      $severity The severity level for this error. A value of 0
616
     *                           will be converted into the default severity level.
617
     * @param boolean  $fixable  Can the error be fixed by the sniff?
618
     *
619
     * @return boolean
620
     */
621
    public function addError(
×
622
        $error,
623
        $stackPtr,
624
        $code,
625
        $data=[],
626
        $severity=0,
627
        $fixable=false
628
    ) {
629
        if ($stackPtr === null) {
×
630
            $line   = 1;
×
631
            $column = 1;
×
632
        } else {
633
            $line   = $this->tokens[$stackPtr]['line'];
×
634
            $column = $this->tokens[$stackPtr]['column'];
×
635
        }
636

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

639
    }//end addError()
640

641

642
    /**
643
     * Records a warning against a specific token in the file.
644
     *
645
     * @param string   $warning  The error message.
646
     * @param int|null $stackPtr The stack position where the error occurred.
647
     * @param string   $code     A violation code unique to the sniff message.
648
     * @param array    $data     Replacements for the warning message.
649
     * @param int      $severity The severity level for this warning. A value of 0
650
     *                           will be converted into the default severity level.
651
     * @param boolean  $fixable  Can the warning be fixed by the sniff?
652
     *
653
     * @return boolean
654
     */
655
    public function addWarning(
×
656
        $warning,
657
        $stackPtr,
658
        $code,
659
        $data=[],
660
        $severity=0,
661
        $fixable=false
662
    ) {
663
        if ($stackPtr === null) {
×
664
            $line   = 1;
×
665
            $column = 1;
×
666
        } else {
667
            $line   = $this->tokens[$stackPtr]['line'];
×
668
            $column = $this->tokens[$stackPtr]['column'];
×
669
        }
670

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

673
    }//end addWarning()
674

675

676
    /**
677
     * Records an error against a specific line in the file.
678
     *
679
     * @param string $error    The error message.
680
     * @param int    $line     The line on which the error occurred.
681
     * @param string $code     A violation code unique to the sniff message.
682
     * @param array  $data     Replacements for the error message.
683
     * @param int    $severity The severity level for this error. A value of 0
684
     *                         will be converted into the default severity level.
685
     *
686
     * @return boolean
687
     */
688
    public function addErrorOnLine(
×
689
        $error,
690
        $line,
691
        $code,
692
        $data=[],
693
        $severity=0
694
    ) {
695
        return $this->addMessage(true, $error, $line, 1, $code, $data, $severity, false);
×
696

697
    }//end addErrorOnLine()
698

699

700
    /**
701
     * Records a warning against a specific line in the file.
702
     *
703
     * @param string $warning  The error message.
704
     * @param int    $line     The line on which the warning occurred.
705
     * @param string $code     A violation code unique to the sniff message.
706
     * @param array  $data     Replacements for the warning message.
707
     * @param int    $severity The severity level for this warning. A value of 0 will
708
     *                         will be converted into the default severity level.
709
     *
710
     * @return boolean
711
     */
712
    public function addWarningOnLine(
×
713
        $warning,
714
        $line,
715
        $code,
716
        $data=[],
717
        $severity=0
718
    ) {
719
        return $this->addMessage(false, $warning, $line, 1, $code, $data, $severity, false);
×
720

721
    }//end addWarningOnLine()
722

723

724
    /**
725
     * Records a fixable error against a specific token in the file.
726
     *
727
     * Returns true if the error was recorded and should be fixed.
728
     *
729
     * @param string $error    The error message.
730
     * @param int    $stackPtr The stack position where the error occurred.
731
     * @param string $code     A violation code unique to the sniff message.
732
     * @param array  $data     Replacements for the error message.
733
     * @param int    $severity The severity level for this error. A value of 0
734
     *                         will be converted into the default severity level.
735
     *
736
     * @return boolean
737
     */
738
    public function addFixableError(
×
739
        $error,
740
        $stackPtr,
741
        $code,
742
        $data=[],
743
        $severity=0
744
    ) {
745
        $recorded = $this->addError($error, $stackPtr, $code, $data, $severity, true);
×
746
        if ($recorded === true && $this->fixer->enabled === true) {
×
747
            return true;
×
748
        }
749

750
        return false;
×
751

752
    }//end addFixableError()
753

754

755
    /**
756
     * Records a fixable warning against a specific token in the file.
757
     *
758
     * Returns true if the warning was recorded and should be fixed.
759
     *
760
     * @param string $warning  The error message.
761
     * @param int    $stackPtr The stack position where the error 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
765
     *                         will be converted into the default severity level.
766
     *
767
     * @return boolean
768
     */
769
    public function addFixableWarning(
×
770
        $warning,
771
        $stackPtr,
772
        $code,
773
        $data=[],
774
        $severity=0
775
    ) {
776
        $recorded = $this->addWarning($warning, $stackPtr, $code, $data, $severity, true);
×
777
        if ($recorded === true && $this->fixer->enabled === true) {
×
778
            return true;
×
779
        }
780

781
        return false;
×
782

783
    }//end addFixableWarning()
784

785

786
    /**
787
     * Adds an error to the error stack.
788
     *
789
     * @param boolean $error    Is this an error message?
790
     * @param string  $message  The text of the message.
791
     * @param int     $line     The line on which the message occurred.
792
     * @param int     $column   The column at which the message occurred.
793
     * @param string  $code     A violation code unique to the sniff message.
794
     * @param array   $data     Replacements for the message.
795
     * @param int     $severity The severity level for this message. A value of 0
796
     *                          will be converted into the default severity level.
797
     * @param boolean $fixable  Can the problem be fixed by the sniff?
798
     *
799
     * @return boolean
800
     */
801
    protected function addMessage($error, $message, $line, $column, $code, $data, $severity, $fixable)
273✔
802
    {
803
        // Check if this line is ignoring all message codes.
804
        if (isset($this->tokenizer->ignoredLines[$line]['.all']) === true) {
273✔
805
            return false;
132✔
806
        }
807

808
        // Work out which sniff generated the message.
809
        $parts = explode('.', $code);
186✔
810
        if ($parts[0] === 'Internal') {
186✔
811
            // An internal message.
812
            $listenerCode = '';
18✔
813
            if ($this->activeListener !== '') {
18✔
814
                $listenerCode = Common::getSniffCode($this->activeListener);
×
815
            }
816

817
            $sniffCode  = $code;
18✔
818
            $checkCodes = [$sniffCode];
18✔
819
        } else {
820
            if ($parts[0] !== $code) {
168✔
821
                // The full message code has been passed in.
822
                $sniffCode    = $code;
×
823
                $listenerCode = substr($sniffCode, 0, strrpos($sniffCode, '.'));
×
824
            } else {
825
                $listenerCode = Common::getSniffCode($this->activeListener);
168✔
826
                $sniffCode    = $listenerCode.'.'.$code;
168✔
827
                $parts        = explode('.', $sniffCode);
168✔
828
            }
829

830
            $checkCodes = [
112✔
831
                $sniffCode,
168✔
832
                $parts[0].'.'.$parts[1].'.'.$parts[2],
168✔
833
                $parts[0].'.'.$parts[1],
168✔
834
                $parts[0],
168✔
835
            ];
112✔
836
        }//end if
837

838
        if (isset($this->tokenizer->ignoredLines[$line]) === true) {
186✔
839
            // Check if this line is ignoring this specific message.
840
            $ignored = false;
99✔
841
            foreach ($checkCodes as $checkCode) {
99✔
842
                if (isset($this->tokenizer->ignoredLines[$line][$checkCode]) === true) {
99✔
843
                    $ignored = true;
90✔
844
                    break;
90✔
845
                }
846
            }
847

848
            // If it is ignored, make sure there is no exception in place.
849
            if ($ignored === true
99✔
850
                && isset($this->tokenizer->ignoredLines[$line]['.except']) === true
99✔
851
            ) {
852
                foreach ($checkCodes as $checkCode) {
15✔
853
                    if (isset($this->tokenizer->ignoredLines[$line]['.except'][$checkCode]) === true) {
15✔
854
                        $ignored = false;
12✔
855
                        break;
12✔
856
                    }
857
                }
858
            }
859

860
            if ($ignored === true) {
99✔
861
                return false;
90✔
862
            }
863
        }//end if
864

865
        $includeAll = true;
174✔
866
        if ($this->configCache['cache'] === false
174✔
867
            || $this->configCache['recordErrors'] === false
174✔
868
        ) {
869
            $includeAll = false;
174✔
870
        }
871

872
        // Filter out any messages for sniffs that shouldn't have run
873
        // due to the use of the --sniffs or --exclude command line argument,
874
        // but don't filter out "Internal" messages.
875
        if ($includeAll === false
174✔
876
            && (($parts[0] !== 'Internal'
174✔
877
            && empty($this->configCache['sniffs']) === false
174✔
878
            && in_array(strtolower($listenerCode), $this->configCache['sniffs'], true) === false)
168✔
879
            || (empty($this->configCache['exclude']) === false
174✔
880
            && in_array(strtolower($listenerCode), $this->configCache['exclude'], true) === true))
174✔
881
        ) {
882
            return false;
×
883
        }
884

885
        // If we know this sniff code is being ignored for this file, return early.
886
        foreach ($checkCodes as $checkCode) {
174✔
887
            if (isset($this->ignoredCodes[$checkCode]) === true) {
174✔
888
                return false;
×
889
            }
890
        }
891

892
        $oppositeType = 'warning';
174✔
893
        if ($error === false) {
174✔
894
            $oppositeType = 'error';
75✔
895
        }
896

897
        foreach ($checkCodes as $checkCode) {
174✔
898
            // Make sure this message type has not been set to the opposite message type.
899
            if (isset($this->ruleset->ruleset[$checkCode]['type']) === true
174✔
900
                && $this->ruleset->ruleset[$checkCode]['type'] === $oppositeType
174✔
901
            ) {
902
                $error = !$error;
×
903
                break;
×
904
            }
905
        }
906

907
        if ($error === true) {
174✔
908
            $configSeverity = $this->configCache['errorSeverity'];
150✔
909
            $messageCount   = &$this->errorCount;
150✔
910
            $messages       = &$this->errors;
150✔
911
        } else {
912
            $configSeverity = $this->configCache['warningSeverity'];
75✔
913
            $messageCount   = &$this->warningCount;
75✔
914
            $messages       = &$this->warnings;
75✔
915
        }
916

917
        if ($includeAll === false && $configSeverity === 0) {
174✔
918
            // Don't bother doing any processing as these messages are just going to
919
            // be hidden in the reports anyway.
920
            return false;
×
921
        }
922

923
        if ($severity === 0) {
174✔
924
            $severity = 5;
174✔
925
        }
926

927
        foreach ($checkCodes as $checkCode) {
174✔
928
            // Make sure we are interested in this severity level.
929
            if (isset($this->ruleset->ruleset[$checkCode]['severity']) === true) {
174✔
930
                $severity = $this->ruleset->ruleset[$checkCode]['severity'];
9✔
931
                break;
9✔
932
            }
933
        }
934

935
        if ($includeAll === false && $configSeverity > $severity) {
174✔
936
            return false;
9✔
937
        }
938

939
        // Make sure we are not ignoring this file.
940
        $included = null;
165✔
941
        if (trim($this->path, '\'"') === 'STDIN') {
165✔
942
            $included = true;
165✔
943
        } else {
944
            foreach ($checkCodes as $checkCode) {
×
945
                $patterns = null;
×
946

947
                if (isset($this->configCache['includePatterns'][$checkCode]) === true) {
×
948
                    $patterns  = $this->configCache['includePatterns'][$checkCode];
×
949
                    $excluding = false;
×
950
                } else if (isset($this->configCache['ignorePatterns'][$checkCode]) === true) {
×
951
                    $patterns  = $this->configCache['ignorePatterns'][$checkCode];
×
952
                    $excluding = true;
×
953
                }
954

955
                if ($patterns === null) {
×
956
                    continue;
×
957
                }
958

959
                foreach ($patterns as $pattern => $type) {
×
960
                    // While there is support for a type of each pattern
961
                    // (absolute or relative) we don't actually support it here.
962
                    $replacements = [
963
                        '\\,' => ',',
×
964
                        '*'   => '.*',
965
                    ];
966

967
                    // We assume a / directory separator, as do the exclude rules
968
                    // most developers write, so we need a special case for any system
969
                    // that is different.
970
                    if (DIRECTORY_SEPARATOR === '\\') {
×
971
                        $replacements['/'] = '\\\\';
×
972
                    }
973

974
                    $pattern = '`'.strtr($pattern, $replacements).'`i';
×
975
                    $matched = preg_match($pattern, $this->path);
×
976

977
                    if ($matched === 0) {
×
978
                        if ($excluding === false && $included === null) {
×
979
                            // This file path is not being included.
980
                            $included = false;
×
981
                        }
982

983
                        continue;
×
984
                    }
985

986
                    if ($excluding === true) {
×
987
                        // This file path is being excluded.
988
                        $this->ignoredCodes[$checkCode] = true;
×
989
                        return false;
×
990
                    }
991

992
                    // This file path is being included.
993
                    $included = true;
×
994
                    break;
×
995
                }//end foreach
996
            }//end foreach
997
        }//end if
998

999
        if ($included === false) {
165✔
1000
            // There were include rules set, but this file
1001
            // path didn't match any of them.
1002
            return false;
×
1003
        }
1004

1005
        $messageCount++;
165✔
1006
        if ($fixable === true) {
165✔
1007
            $this->fixableCount++;
132✔
1008
        }
1009

1010
        if ($this->configCache['recordErrors'] === false
165✔
1011
            && $includeAll === false
165✔
1012
        ) {
1013
            return true;
×
1014
        }
1015

1016
        // See if there is a custom error message format to use.
1017
        // But don't do this if we are replaying errors because replayed
1018
        // errors have already used the custom format and have had their
1019
        // data replaced.
1020
        if ($this->replayingErrors === false
165✔
1021
            && isset($this->ruleset->ruleset[$sniffCode]['message']) === true
165✔
1022
        ) {
1023
            $message = $this->ruleset->ruleset[$sniffCode]['message'];
×
1024
        }
1025

1026
        if (empty($data) === false) {
165✔
1027
            $message = vsprintf($message, $data);
153✔
1028
        }
1029

1030
        if (isset($messages[$line]) === false) {
165✔
1031
            $messages[$line] = [];
165✔
1032
        }
1033

1034
        if (isset($messages[$line][$column]) === false) {
165✔
1035
            $messages[$line][$column] = [];
165✔
1036
        }
1037

1038
        $messages[$line][$column][] = [
165✔
1039
            'message'  => $message,
165✔
1040
            'source'   => $sniffCode,
165✔
1041
            'listener' => $this->activeListener,
165✔
1042
            'severity' => $severity,
165✔
1043
            'fixable'  => $fixable,
165✔
1044
        ];
110✔
1045

1046
        if (PHP_CODESNIFFER_VERBOSITY > 1
165✔
1047
            && $this->fixer->enabled === true
165✔
1048
            && $fixable === true
165✔
1049
        ) {
1050
            @ob_end_clean();
×
NEW
1051
            StatusWriter::forceWrite("E: [Line $line] $message ($sniffCode)", 1);
×
1052
            ob_start();
×
1053
        }
1054

1055
        return true;
165✔
1056

1057
    }//end addMessage()
1058

1059

1060
    /**
1061
     * Record a metric about the file being examined.
1062
     *
1063
     * @param int    $stackPtr The stack position where the metric was recorded.
1064
     * @param string $metric   The name of the metric being recorded.
1065
     * @param string $value    The value of the metric being recorded.
1066
     *
1067
     * @return boolean
1068
     */
1069
    public function recordMetric($stackPtr, $metric, $value)
×
1070
    {
1071
        if (isset($this->metrics[$metric]) === false) {
×
1072
            $this->metrics[$metric] = ['values' => [$value => 1]];
×
1073
            $this->metricTokens[$metric][$stackPtr] = true;
×
1074
        } else if (isset($this->metricTokens[$metric][$stackPtr]) === false) {
×
1075
            $this->metricTokens[$metric][$stackPtr] = true;
×
1076
            if (isset($this->metrics[$metric]['values'][$value]) === false) {
×
1077
                $this->metrics[$metric]['values'][$value] = 1;
×
1078
            } else {
1079
                $this->metrics[$metric]['values'][$value]++;
×
1080
            }
1081
        }
1082

1083
        return true;
×
1084

1085
    }//end recordMetric()
1086

1087

1088
    /**
1089
     * Returns the number of errors raised.
1090
     *
1091
     * @return int
1092
     */
1093
    public function getErrorCount()
×
1094
    {
1095
        return $this->errorCount;
×
1096

1097
    }//end getErrorCount()
1098

1099

1100
    /**
1101
     * Returns the number of warnings raised.
1102
     *
1103
     * @return int
1104
     */
1105
    public function getWarningCount()
×
1106
    {
1107
        return $this->warningCount;
×
1108

1109
    }//end getWarningCount()
1110

1111

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

1121
    }//end getFixableCount()
1122

1123

1124
    /**
1125
     * Returns the number of fixed errors/warnings.
1126
     *
1127
     * @return int
1128
     */
1129
    public function getFixedCount()
×
1130
    {
1131
        return $this->fixedCount;
×
1132

1133
    }//end getFixedCount()
1134

1135

1136
    /**
1137
     * Returns the list of ignored lines.
1138
     *
1139
     * @return array
1140
     */
1141
    public function getIgnoredLines()
×
1142
    {
1143
        return $this->tokenizer->ignoredLines;
×
1144

1145
    }//end getIgnoredLines()
1146

1147

1148
    /**
1149
     * Returns the errors raised from processing this file.
1150
     *
1151
     * @return array
1152
     */
1153
    public function getErrors()
×
1154
    {
1155
        return $this->errors;
×
1156

1157
    }//end getErrors()
1158

1159

1160
    /**
1161
     * Returns the warnings raised from processing this file.
1162
     *
1163
     * @return array
1164
     */
1165
    public function getWarnings()
×
1166
    {
1167
        return $this->warnings;
×
1168

1169
    }//end getWarnings()
1170

1171

1172
    /**
1173
     * Returns the metrics found while processing this file.
1174
     *
1175
     * @return array
1176
     */
1177
    public function getMetrics()
×
1178
    {
1179
        return $this->metrics;
×
1180

1181
    }//end getMetrics()
1182

1183

1184
    /**
1185
     * Returns the time taken processing this file for each invoked sniff.
1186
     *
1187
     * @return array
1188
     */
1189
    public function getListenerTimes()
×
1190
    {
1191
        return $this->listenerTimes;
×
1192

1193
    }//end getListenerTimes()
1194

1195

1196
    /**
1197
     * Returns the absolute filename of this file.
1198
     *
1199
     * @return string
1200
     */
1201
    public function getFilename()
×
1202
    {
1203
        return $this->path;
×
1204

1205
    }//end getFilename()
1206

1207

1208
    /**
1209
     * Returns the declaration name for classes, interfaces, traits, enums, and functions.
1210
     *
1211
     * @param int $stackPtr The position of the declaration token which
1212
     *                      declared the class, interface, trait, or function.
1213
     *
1214
     * @return string The name of the class, interface, trait, or function or an empty string
1215
     *                if the name could not be determined (live coding).
1216
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type
1217
     *                                                      T_FUNCTION, T_CLASS, T_TRAIT, T_ENUM, or T_INTERFACE.
1218
     */
1219
    public function getDeclarationName($stackPtr)
87✔
1220
    {
1221
        $tokenCode = $this->tokens[$stackPtr]['code'];
87✔
1222

1223
        if ($tokenCode !== T_FUNCTION
87✔
1224
            && $tokenCode !== T_CLASS
87✔
1225
            && $tokenCode !== T_INTERFACE
87✔
1226
            && $tokenCode !== T_TRAIT
87✔
1227
            && $tokenCode !== T_ENUM
87✔
1228
        ) {
1229
            throw new RuntimeException('Token type "'.$this->tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM');
18✔
1230
        }
1231

1232
        $stopPoint = $this->numTokens;
69✔
1233
        if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === true) {
69✔
1234
            // For functions, stop searching at the parenthesis opener.
1235
            $stopPoint = $this->tokens[$stackPtr]['parenthesis_opener'];
36✔
1236
        } else if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
33✔
1237
            // For OO tokens, stop searching at the open curly.
1238
            $stopPoint = $this->tokens[$stackPtr]['scope_opener'];
30✔
1239
        }
1240

1241
        $content = '';
69✔
1242
        for ($i = $stackPtr; $i < $stopPoint; $i++) {
69✔
1243
            if ($this->tokens[$i]['code'] === T_STRING) {
69✔
1244
                $content = $this->tokens[$i]['content'];
63✔
1245
                break;
63✔
1246
            }
1247
        }
1248

1249
        return $content;
69✔
1250

1251
    }//end getDeclarationName()
1252

1253

1254
    /**
1255
     * Returns the method parameters for the specified function token.
1256
     *
1257
     * Also supports passing in a USE token for a closure use group.
1258
     *
1259
     * Each parameter is in the following format:
1260
     *
1261
     * <code>
1262
     *   0 => array(
1263
     *         'name'                => string,        // The variable name.
1264
     *         'token'               => integer,       // The stack pointer to the variable name.
1265
     *         'content'             => string,        // The full content of the variable definition.
1266
     *         'has_attributes'      => boolean,       // Does the parameter have one or more attributes attached ?
1267
     *         'pass_by_reference'   => boolean,       // Is the variable passed by reference?
1268
     *         'reference_token'     => integer|false, // The stack pointer to the reference operator
1269
     *                                                 // or FALSE if the param is not passed by reference.
1270
     *         'variable_length'     => boolean,       // Is the param of variable length through use of `...` ?
1271
     *         'variadic_token'      => integer|false, // The stack pointer to the ... operator
1272
     *                                                 // or FALSE if the param is not variable length.
1273
     *         'type_hint'           => string,        // The type hint for the variable.
1274
     *         'type_hint_token'     => integer|false, // The stack pointer to the start of the type hint
1275
     *                                                 // or FALSE if there is no type hint.
1276
     *         'type_hint_end_token' => integer|false, // The stack pointer to the end of the type hint
1277
     *                                                 // or FALSE if there is no type hint.
1278
     *         'nullable_type'       => boolean,       // TRUE if the type is preceded by the nullability
1279
     *                                                 // operator.
1280
     *         'comma_token'         => integer|false, // The stack pointer to the comma after the param
1281
     *                                                 // or FALSE if this is the last param.
1282
     *        )
1283
     * </code>
1284
     *
1285
     * Parameters with default values have additional array indexes of:
1286
     *         'default'             => string,  // The full content of the default value.
1287
     *         'default_token'       => integer, // The stack pointer to the start of the default value.
1288
     *         'default_equal_token' => integer, // The stack pointer to the equals sign.
1289
     *
1290
     * Parameters declared using PHP 8 constructor property promotion, have these additional array indexes:
1291
     *         'property_visibility' => string,        // The property visibility as declared.
1292
     *         'visibility_token'    => integer|false, // The stack pointer to the visibility modifier token
1293
     *                                                 // or FALSE if the visibility is not explicitly declared.
1294
     *         'property_readonly'   => boolean,       // TRUE if the readonly keyword was found.
1295
     *         'readonly_token'      => integer,       // The stack pointer to the readonly modifier token.
1296
     *                                                 // This index will only be set if the property is readonly.
1297
     *
1298
     * @param int $stackPtr The position in the stack of the function token
1299
     *                      to acquire the parameters for.
1300
     *
1301
     * @return array
1302
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified $stackPtr is not of
1303
     *                                                      type T_FUNCTION, T_CLOSURE, T_USE,
1304
     *                                                      or T_FN.
1305
     */
1306
    public function getMethodParameters($stackPtr)
231✔
1307
    {
1308
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
231✔
1309
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
231✔
1310
            && $this->tokens[$stackPtr]['code'] !== T_USE
231✔
1311
            && $this->tokens[$stackPtr]['code'] !== T_FN
231✔
1312
        ) {
1313
            throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_USE or T_FN');
9✔
1314
        }
1315

1316
        if ($this->tokens[$stackPtr]['code'] === T_USE) {
222✔
1317
            $opener = $this->findNext(T_OPEN_PARENTHESIS, ($stackPtr + 1));
21✔
1318
            if ($opener === false || isset($this->tokens[$opener]['parenthesis_owner']) === true) {
21✔
1319
                throw new RuntimeException('$stackPtr was not a valid T_USE');
17✔
1320
            }
1321
        } else {
1322
            if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === false) {
201✔
1323
                // Live coding or syntax error, so no params to find.
1324
                return [];
3✔
1325
            }
1326

1327
            $opener = $this->tokens[$stackPtr]['parenthesis_opener'];
198✔
1328
        }
1329

1330
        if (isset($this->tokens[$opener]['parenthesis_closer']) === false) {
210✔
1331
            // Live coding or syntax error, so no params to find.
1332
            return [];
3✔
1333
        }
1334

1335
        $closer = $this->tokens[$opener]['parenthesis_closer'];
207✔
1336

1337
        $vars            = [];
207✔
1338
        $currVar         = null;
207✔
1339
        $paramStart      = ($opener + 1);
207✔
1340
        $defaultStart    = null;
207✔
1341
        $equalToken      = null;
207✔
1342
        $paramCount      = 0;
207✔
1343
        $hasAttributes   = false;
207✔
1344
        $passByReference = false;
207✔
1345
        $referenceToken  = false;
207✔
1346
        $variableLength  = false;
207✔
1347
        $variadicToken   = false;
207✔
1348
        $typeHint        = '';
207✔
1349
        $typeHintToken   = false;
207✔
1350
        $typeHintEndToken = false;
207✔
1351
        $nullableType     = false;
207✔
1352
        $visibilityToken  = null;
207✔
1353
        $readonlyToken    = null;
207✔
1354

1355
        for ($i = $paramStart; $i <= $closer; $i++) {
207✔
1356
            // Check to see if this token has a parenthesis or bracket opener. If it does
1357
            // it's likely to be an array which might have arguments in it. This
1358
            // could cause problems in our parsing below, so lets just skip to the
1359
            // end of it.
1360
            if ($this->tokens[$i]['code'] !== T_TYPE_OPEN_PARENTHESIS
207✔
1361
                && isset($this->tokens[$i]['parenthesis_opener']) === true
207✔
1362
            ) {
1363
                // Don't do this if it's the close parenthesis for the method.
1364
                if ($i !== $this->tokens[$i]['parenthesis_closer']) {
207✔
1365
                    $i = $this->tokens[$i]['parenthesis_closer'];
9✔
1366
                    continue;
9✔
1367
                }
1368
            }
1369

1370
            if (isset($this->tokens[$i]['bracket_opener']) === true) {
207✔
1371
                if ($i !== $this->tokens[$i]['bracket_closer']) {
3✔
1372
                    $i = $this->tokens[$i]['bracket_closer'];
3✔
1373
                    continue;
3✔
1374
                }
1375
            }
1376

1377
            switch ($this->tokens[$i]['code']) {
207✔
1378
            case T_ATTRIBUTE:
207✔
1379
                $hasAttributes = true;
6✔
1380

1381
                // Skip to the end of the attribute.
1382
                $i = $this->tokens[$i]['attribute_closer'];
6✔
1383
                break;
6✔
1384
            case T_BITWISE_AND:
207✔
1385
                if ($defaultStart === null) {
54✔
1386
                    $passByReference = true;
51✔
1387
                    $referenceToken  = $i;
51✔
1388
                }
1389
                break;
54✔
1390
            case T_VARIABLE:
207✔
1391
                $currVar = $i;
198✔
1392
                break;
198✔
1393
            case T_ELLIPSIS:
207✔
1394
                $variableLength = true;
51✔
1395
                $variadicToken  = $i;
51✔
1396
                break;
51✔
1397
            case T_CALLABLE:
207✔
1398
                if ($typeHintToken === false) {
12✔
1399
                    $typeHintToken = $i;
9✔
1400
                }
1401

1402
                $typeHint        .= $this->tokens[$i]['content'];
12✔
1403
                $typeHintEndToken = $i;
12✔
1404
                break;
12✔
1405
            case T_SELF:
207✔
1406
            case T_PARENT:
207✔
1407
            case T_STATIC:
207✔
1408
                // Self and parent are valid, static invalid, but was probably intended as type hint.
1409
                if (isset($defaultStart) === false) {
18✔
1410
                    if ($typeHintToken === false) {
15✔
1411
                        $typeHintToken = $i;
12✔
1412
                    }
1413

1414
                    $typeHint        .= $this->tokens[$i]['content'];
15✔
1415
                    $typeHintEndToken = $i;
15✔
1416
                }
1417
                break;
18✔
1418
            case T_STRING:
207✔
1419
                // This is a string, so it may be a type hint, but it could
1420
                // also be a constant used as a default value.
1421
                $prevComma = false;
138✔
1422
                for ($t = $i; $t >= $opener; $t--) {
138✔
1423
                    if ($this->tokens[$t]['code'] === T_COMMA) {
138✔
1424
                        $prevComma = $t;
60✔
1425
                        break;
60✔
1426
                    }
1427
                }
1428

1429
                if ($prevComma !== false) {
138✔
1430
                    $nextEquals = false;
60✔
1431
                    for ($t = $prevComma; $t < $i; $t++) {
60✔
1432
                        if ($this->tokens[$t]['code'] === T_EQUAL) {
60✔
1433
                            $nextEquals = $t;
9✔
1434
                            break;
9✔
1435
                        }
1436
                    }
1437

1438
                    if ($nextEquals !== false) {
60✔
1439
                        break;
9✔
1440
                    }
1441
                }
1442

1443
                if ($defaultStart === null) {
135✔
1444
                    if ($typeHintToken === false) {
132✔
1445
                        $typeHintToken = $i;
114✔
1446
                    }
1447

1448
                    $typeHint        .= $this->tokens[$i]['content'];
132✔
1449
                    $typeHintEndToken = $i;
132✔
1450
                }
1451
                break;
135✔
1452
            case T_NAMESPACE:
207✔
1453
            case T_NS_SEPARATOR:
207✔
1454
            case T_TYPE_UNION:
207✔
1455
            case T_TYPE_INTERSECTION:
207✔
1456
            case T_TYPE_OPEN_PARENTHESIS:
207✔
1457
            case T_TYPE_CLOSE_PARENTHESIS:
207✔
1458
            case T_FALSE:
207✔
1459
            case T_TRUE:
207✔
1460
            case T_NULL:
207✔
1461
                // Part of a type hint or default value.
1462
                if ($defaultStart === null) {
108✔
1463
                    if ($typeHintToken === false) {
99✔
1464
                        $typeHintToken = $i;
42✔
1465
                    }
1466

1467
                    $typeHint        .= $this->tokens[$i]['content'];
99✔
1468
                    $typeHintEndToken = $i;
99✔
1469
                }
1470
                break;
108✔
1471
            case T_NULLABLE:
207✔
1472
                if ($defaultStart === null) {
63✔
1473
                    $nullableType     = true;
63✔
1474
                    $typeHint        .= $this->tokens[$i]['content'];
63✔
1475
                    $typeHintEndToken = $i;
63✔
1476
                }
1477
                break;
63✔
1478
            case T_PUBLIC:
207✔
1479
            case T_PROTECTED:
207✔
1480
            case T_PRIVATE:
207✔
1481
                if ($defaultStart === null) {
24✔
1482
                    $visibilityToken = $i;
24✔
1483
                }
1484
                break;
24✔
1485
            case T_READONLY:
207✔
1486
                if ($defaultStart === null) {
9✔
1487
                    $readonlyToken = $i;
9✔
1488
                }
1489
                break;
9✔
1490
            case T_CLOSE_PARENTHESIS:
207✔
1491
            case T_COMMA:
192✔
1492
                // If it's null, then there must be no parameters for this
1493
                // method.
1494
                if ($currVar === null) {
207✔
1495
                    continue 2;
33✔
1496
                }
1497

1498
                $vars[$paramCount]            = [];
198✔
1499
                $vars[$paramCount]['token']   = $currVar;
198✔
1500
                $vars[$paramCount]['name']    = $this->tokens[$currVar]['content'];
198✔
1501
                $vars[$paramCount]['content'] = trim($this->getTokensAsString($paramStart, ($i - $paramStart)));
198✔
1502

1503
                if ($defaultStart !== null) {
198✔
1504
                    $vars[$paramCount]['default']       = trim($this->getTokensAsString($defaultStart, ($i - $defaultStart)));
66✔
1505
                    $vars[$paramCount]['default_token'] = $defaultStart;
66✔
1506
                    $vars[$paramCount]['default_equal_token'] = $equalToken;
66✔
1507
                }
1508

1509
                $vars[$paramCount]['has_attributes']      = $hasAttributes;
198✔
1510
                $vars[$paramCount]['pass_by_reference']   = $passByReference;
198✔
1511
                $vars[$paramCount]['reference_token']     = $referenceToken;
198✔
1512
                $vars[$paramCount]['variable_length']     = $variableLength;
198✔
1513
                $vars[$paramCount]['variadic_token']      = $variadicToken;
198✔
1514
                $vars[$paramCount]['type_hint']           = $typeHint;
198✔
1515
                $vars[$paramCount]['type_hint_token']     = $typeHintToken;
198✔
1516
                $vars[$paramCount]['type_hint_end_token'] = $typeHintEndToken;
198✔
1517
                $vars[$paramCount]['nullable_type']       = $nullableType;
198✔
1518

1519
                if ($visibilityToken !== null || $readonlyToken !== null) {
198✔
1520
                    $vars[$paramCount]['property_visibility'] = 'public';
27✔
1521
                    $vars[$paramCount]['visibility_token']    = false;
27✔
1522
                    $vars[$paramCount]['property_readonly']   = false;
27✔
1523

1524
                    if ($visibilityToken !== null) {
27✔
1525
                        $vars[$paramCount]['property_visibility'] = $this->tokens[$visibilityToken]['content'];
24✔
1526
                        $vars[$paramCount]['visibility_token']    = $visibilityToken;
24✔
1527
                    }
1528

1529
                    if ($readonlyToken !== null) {
27✔
1530
                        $vars[$paramCount]['property_readonly'] = true;
9✔
1531
                        $vars[$paramCount]['readonly_token']    = $readonlyToken;
9✔
1532
                    }
1533
                }
1534

1535
                if ($this->tokens[$i]['code'] === T_COMMA) {
198✔
1536
                    $vars[$paramCount]['comma_token'] = $i;
99✔
1537
                } else {
1538
                    $vars[$paramCount]['comma_token'] = false;
174✔
1539
                }
1540

1541
                // Reset the vars, as we are about to process the next parameter.
1542
                $currVar          = null;
198✔
1543
                $paramStart       = ($i + 1);
198✔
1544
                $defaultStart     = null;
198✔
1545
                $equalToken       = null;
198✔
1546
                $hasAttributes    = false;
198✔
1547
                $passByReference  = false;
198✔
1548
                $referenceToken   = false;
198✔
1549
                $variableLength   = false;
198✔
1550
                $variadicToken    = false;
198✔
1551
                $typeHint         = '';
198✔
1552
                $typeHintToken    = false;
198✔
1553
                $typeHintEndToken = false;
198✔
1554
                $nullableType     = false;
198✔
1555
                $visibilityToken  = null;
198✔
1556
                $readonlyToken    = null;
198✔
1557

1558
                $paramCount++;
198✔
1559
                break;
198✔
1560
            case T_EQUAL:
192✔
1561
                $defaultStart = $this->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
66✔
1562
                $equalToken   = $i;
66✔
1563
                break;
66✔
1564
            }//end switch
1565
        }//end for
1566

1567
        return $vars;
207✔
1568

1569
    }//end getMethodParameters()
1570

1571

1572
    /**
1573
     * Returns the visibility and implementation properties of a method.
1574
     *
1575
     * The format of the return value is:
1576
     * <code>
1577
     *   array(
1578
     *    'scope'                 => string,        // Public, private, or protected
1579
     *    'scope_specified'       => boolean,       // TRUE if the scope keyword was found.
1580
     *    'return_type'           => string,        // The return type of the method.
1581
     *    'return_type_token'     => integer|false, // The stack pointer to the start of the return type
1582
     *                                              // or FALSE if there is no return type.
1583
     *    'return_type_end_token' => integer|false, // The stack pointer to the end of the return type
1584
     *                                              // or FALSE if there is no return type.
1585
     *    'nullable_return_type'  => boolean,       // TRUE if the return type is preceded by the
1586
     *                                              // nullability operator.
1587
     *    'is_abstract'           => boolean,       // TRUE if the abstract keyword was found.
1588
     *    'is_final'              => boolean,       // TRUE if the final keyword was found.
1589
     *    'is_static'             => boolean,       // TRUE if the static keyword was found.
1590
     *    'has_body'              => boolean,       // TRUE if the method has a body
1591
     *   );
1592
     * </code>
1593
     *
1594
     * @param int $stackPtr The position in the stack of the function token to
1595
     *                      acquire the properties for.
1596
     *
1597
     * @return array
1598
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1599
     *                                                      T_FUNCTION, T_CLOSURE, or T_FN token.
1600
     */
1601
    public function getMethodProperties($stackPtr)
177✔
1602
    {
1603
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
177✔
1604
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
177✔
1605
            && $this->tokens[$stackPtr]['code'] !== T_FN
177✔
1606
        ) {
1607
            throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_FN');
9✔
1608
        }
1609

1610
        if ($this->tokens[$stackPtr]['code'] === T_FUNCTION) {
168✔
1611
            $valid = [
82✔
1612
                T_PUBLIC     => T_PUBLIC,
123✔
1613
                T_PRIVATE    => T_PRIVATE,
123✔
1614
                T_PROTECTED  => T_PROTECTED,
123✔
1615
                T_STATIC     => T_STATIC,
123✔
1616
                T_FINAL      => T_FINAL,
123✔
1617
                T_ABSTRACT   => T_ABSTRACT,
123✔
1618
                T_WHITESPACE => T_WHITESPACE,
123✔
1619
                T_COMMENT    => T_COMMENT,
123✔
1620
            ];
82✔
1621
        } else {
1622
            $valid = [
30✔
1623
                T_STATIC     => T_STATIC,
45✔
1624
                T_WHITESPACE => T_WHITESPACE,
45✔
1625
                T_COMMENT    => T_COMMENT,
45✔
1626
            ];
30✔
1627
        }
1628

1629
        $scope          = 'public';
168✔
1630
        $scopeSpecified = false;
168✔
1631
        $isAbstract     = false;
168✔
1632
        $isFinal        = false;
168✔
1633
        $isStatic       = false;
168✔
1634

1635
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
168✔
1636
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
168✔
1637
                break;
165✔
1638
            }
1639

1640
            switch ($this->tokens[$i]['code']) {
165✔
1641
            case T_PUBLIC:
165✔
1642
                $scope          = 'public';
18✔
1643
                $scopeSpecified = true;
18✔
1644
                break;
18✔
1645
            case T_PRIVATE:
165✔
1646
                $scope          = 'private';
9✔
1647
                $scopeSpecified = true;
9✔
1648
                break;
9✔
1649
            case T_PROTECTED:
165✔
1650
                $scope          = 'protected';
9✔
1651
                $scopeSpecified = true;
9✔
1652
                break;
9✔
1653
            case T_ABSTRACT:
165✔
1654
                $isAbstract = true;
9✔
1655
                break;
9✔
1656
            case T_FINAL:
165✔
1657
                $isFinal = true;
3✔
1658
                break;
3✔
1659
            case T_STATIC:
165✔
1660
                $isStatic = true;
6✔
1661
                break;
6✔
1662
            }//end switch
1663
        }//end for
1664

1665
        $returnType         = '';
168✔
1666
        $returnTypeToken    = false;
168✔
1667
        $returnTypeEndToken = false;
168✔
1668
        $nullableReturnType = false;
168✔
1669
        $hasBody            = true;
168✔
1670

1671
        if (isset($this->tokens[$stackPtr]['parenthesis_closer']) === true) {
168✔
1672
            $scopeOpener = null;
168✔
1673
            if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
168✔
1674
                $scopeOpener = $this->tokens[$stackPtr]['scope_opener'];
153✔
1675
            }
1676

1677
            $valid = [
112✔
1678
                T_STRING                 => T_STRING,
168✔
1679
                T_CALLABLE               => T_CALLABLE,
168✔
1680
                T_SELF                   => T_SELF,
168✔
1681
                T_PARENT                 => T_PARENT,
168✔
1682
                T_STATIC                 => T_STATIC,
168✔
1683
                T_FALSE                  => T_FALSE,
168✔
1684
                T_TRUE                   => T_TRUE,
168✔
1685
                T_NULL                   => T_NULL,
168✔
1686
                T_NAMESPACE              => T_NAMESPACE,
168✔
1687
                T_NS_SEPARATOR           => T_NS_SEPARATOR,
168✔
1688
                T_TYPE_UNION             => T_TYPE_UNION,
168✔
1689
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
168✔
1690
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
168✔
1691
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
168✔
1692
            ];
112✔
1693

1694
            for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) {
168✔
1695
                if (($scopeOpener === null && $this->tokens[$i]['code'] === T_SEMICOLON)
168✔
1696
                    || ($scopeOpener !== null && $i === $scopeOpener)
168✔
1697
                ) {
1698
                    // End of function definition.
1699
                    break;
168✔
1700
                }
1701

1702
                if ($this->tokens[$i]['code'] === T_USE) {
168✔
1703
                    // Skip over closure use statements.
1704
                    for ($j = ($i + 1); $j < $this->numTokens && isset(Tokens::$emptyTokens[$this->tokens[$j]['code']]) === true; $j++);
15✔
1705
                    if ($this->tokens[$j]['code'] === T_OPEN_PARENTHESIS) {
15✔
1706
                        if (isset($this->tokens[$j]['parenthesis_closer']) === false) {
15✔
1707
                            // Live coding/parse error, stop parsing.
1708
                            break;
×
1709
                        }
1710

1711
                        $i = $this->tokens[$j]['parenthesis_closer'];
15✔
1712
                        continue;
15✔
1713
                    }
1714
                }
1715

1716
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
168✔
1717
                    $nullableReturnType = true;
39✔
1718
                }
1719

1720
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
168✔
1721
                    if ($returnTypeToken === false) {
144✔
1722
                        $returnTypeToken = $i;
144✔
1723
                    }
1724

1725
                    $returnType        .= $this->tokens[$i]['content'];
144✔
1726
                    $returnTypeEndToken = $i;
144✔
1727
                }
1728
            }//end for
1729

1730
            if ($this->tokens[$stackPtr]['code'] === T_FN) {
168✔
1731
                $bodyToken = T_FN_ARROW;
18✔
1732
            } else {
1733
                $bodyToken = T_OPEN_CURLY_BRACKET;
150✔
1734
            }
1735

1736
            $end     = $this->findNext([$bodyToken, T_SEMICOLON], $this->tokens[$stackPtr]['parenthesis_closer']);
168✔
1737
            $hasBody = $this->tokens[$end]['code'] === $bodyToken;
168✔
1738
        }//end if
1739

1740
        if ($returnType !== '' && $nullableReturnType === true) {
168✔
1741
            $returnType = '?'.$returnType;
39✔
1742
        }
1743

1744
        return [
112✔
1745
            'scope'                 => $scope,
168✔
1746
            'scope_specified'       => $scopeSpecified,
168✔
1747
            'return_type'           => $returnType,
168✔
1748
            'return_type_token'     => $returnTypeToken,
168✔
1749
            'return_type_end_token' => $returnTypeEndToken,
168✔
1750
            'nullable_return_type'  => $nullableReturnType,
168✔
1751
            'is_abstract'           => $isAbstract,
168✔
1752
            'is_final'              => $isFinal,
168✔
1753
            'is_static'             => $isStatic,
168✔
1754
            'has_body'              => $hasBody,
168✔
1755
        ];
112✔
1756

1757
    }//end getMethodProperties()
1758

1759

1760
    /**
1761
     * Returns the visibility and implementation properties of a class member var.
1762
     *
1763
     * The format of the return value is:
1764
     *
1765
     * <code>
1766
     *   array(
1767
     *    'scope'           => string,        // Public, private, or protected.
1768
     *    'scope_specified' => boolean,       // TRUE if the scope was explicitly specified.
1769
     *    'is_static'       => boolean,       // TRUE if the static keyword was found.
1770
     *    'is_readonly'     => boolean,       // TRUE if the readonly keyword was found.
1771
     *    'is_final'        => boolean,       // TRUE if the final keyword was found.
1772
     *    'type'            => string,        // The type of the var (empty if no type specified).
1773
     *    'type_token'      => integer|false, // The stack pointer to the start of the type
1774
     *                                        // or FALSE if there is no type.
1775
     *    'type_end_token'  => integer|false, // The stack pointer to the end of the type
1776
     *                                        // or FALSE if there is no type.
1777
     *    'nullable_type'   => boolean,       // TRUE if the type is preceded by the nullability
1778
     *                                        // operator.
1779
     *   );
1780
     * </code>
1781
     *
1782
     * @param int $stackPtr The position in the stack of the T_VARIABLE token to
1783
     *                      acquire the properties for.
1784
     *
1785
     * @return array
1786
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1787
     *                                                      T_VARIABLE token, or if the position is not
1788
     *                                                      a class member variable.
1789
     */
1790
    public function getMemberProperties($stackPtr)
300✔
1791
    {
1792
        if ($this->tokens[$stackPtr]['code'] !== T_VARIABLE) {
300✔
1793
            throw new RuntimeException('$stackPtr must be of type T_VARIABLE');
3✔
1794
        }
1795

1796
        $conditions = $this->tokens[$stackPtr]['conditions'];
297✔
1797
        $conditions = array_keys($conditions);
297✔
1798
        $ptr        = array_pop($conditions);
297✔
1799
        if (isset($this->tokens[$ptr]) === false
297✔
1800
            || isset(Tokens::$ooScopeTokens[$this->tokens[$ptr]['code']]) === false
294✔
1801
            || $this->tokens[$ptr]['code'] === T_ENUM
297✔
1802
        ) {
1803
            throw new RuntimeException('$stackPtr is not a class member var');
15✔
1804
        }
1805

1806
        // Make sure it's not a method parameter.
1807
        if (empty($this->tokens[$stackPtr]['nested_parenthesis']) === false) {
282✔
1808
            $parenthesis = array_keys($this->tokens[$stackPtr]['nested_parenthesis']);
15✔
1809
            $deepestOpen = array_pop($parenthesis);
15✔
1810
            if ($deepestOpen > $ptr
15✔
1811
                && isset($this->tokens[$deepestOpen]['parenthesis_owner']) === true
15✔
1812
                && $this->tokens[$this->tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION
15✔
1813
            ) {
1814
                throw new RuntimeException('$stackPtr is not a class member var');
9✔
1815
            }
1816
        }
1817

1818
        $valid = [
182✔
1819
            T_PUBLIC    => T_PUBLIC,
273✔
1820
            T_PRIVATE   => T_PRIVATE,
273✔
1821
            T_PROTECTED => T_PROTECTED,
273✔
1822
            T_STATIC    => T_STATIC,
273✔
1823
            T_VAR       => T_VAR,
273✔
1824
            T_READONLY  => T_READONLY,
273✔
1825
            T_FINAL     => T_FINAL,
273✔
1826
        ];
182✔
1827

1828
        $valid += Tokens::$emptyTokens;
273✔
1829

1830
        $scope          = 'public';
273✔
1831
        $scopeSpecified = false;
273✔
1832
        $isStatic       = false;
273✔
1833
        $isReadonly     = false;
273✔
1834
        $isFinal        = false;
273✔
1835

1836
        $startOfStatement = $this->findPrevious(
273✔
1837
            [
182✔
1838
                T_SEMICOLON,
273✔
1839
                T_OPEN_CURLY_BRACKET,
273✔
1840
                T_CLOSE_CURLY_BRACKET,
273✔
1841
                T_ATTRIBUTE_END,
273✔
1842
            ],
182✔
1843
            ($stackPtr - 1)
273✔
1844
        );
182✔
1845

1846
        for ($i = ($startOfStatement + 1); $i < $stackPtr; $i++) {
273✔
1847
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
273✔
1848
                break;
207✔
1849
            }
1850

1851
            switch ($this->tokens[$i]['code']) {
273✔
1852
            case T_PUBLIC:
273✔
1853
                $scope          = 'public';
129✔
1854
                $scopeSpecified = true;
129✔
1855
                break;
129✔
1856
            case T_PRIVATE:
273✔
1857
                $scope          = 'private';
57✔
1858
                $scopeSpecified = true;
57✔
1859
                break;
57✔
1860
            case T_PROTECTED:
273✔
1861
                $scope          = 'protected';
45✔
1862
                $scopeSpecified = true;
45✔
1863
                break;
45✔
1864
            case T_STATIC:
273✔
1865
                $isStatic = true;
66✔
1866
                break;
66✔
1867
            case T_READONLY:
273✔
1868
                $isReadonly = true;
36✔
1869
                break;
36✔
1870
            case T_FINAL:
273✔
1871
                $isFinal = true;
27✔
1872
                break;
27✔
1873
            }//end switch
1874
        }//end for
1875

1876
        $type         = '';
273✔
1877
        $typeToken    = false;
273✔
1878
        $typeEndToken = false;
273✔
1879
        $nullableType = false;
273✔
1880

1881
        if ($i < $stackPtr) {
273✔
1882
            // We've found a type.
1883
            $valid = [
138✔
1884
                T_STRING                 => T_STRING,
207✔
1885
                T_CALLABLE               => T_CALLABLE,
207✔
1886
                T_SELF                   => T_SELF,
207✔
1887
                T_PARENT                 => T_PARENT,
207✔
1888
                T_FALSE                  => T_FALSE,
207✔
1889
                T_TRUE                   => T_TRUE,
207✔
1890
                T_NULL                   => T_NULL,
207✔
1891
                T_NAMESPACE              => T_NAMESPACE,
207✔
1892
                T_NS_SEPARATOR           => T_NS_SEPARATOR,
207✔
1893
                T_TYPE_UNION             => T_TYPE_UNION,
207✔
1894
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
207✔
1895
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
207✔
1896
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
207✔
1897
            ];
138✔
1898

1899
            for ($i; $i < $stackPtr; $i++) {
207✔
1900
                if ($this->tokens[$i]['code'] === T_VARIABLE) {
207✔
1901
                    // Hit another variable in a group definition.
1902
                    break;
30✔
1903
                }
1904

1905
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
183✔
1906
                    $nullableType = true;
51✔
1907
                }
1908

1909
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
183✔
1910
                    $typeEndToken = $i;
183✔
1911
                    if ($typeToken === false) {
183✔
1912
                        $typeToken = $i;
183✔
1913
                    }
1914

1915
                    $type .= $this->tokens[$i]['content'];
183✔
1916
                }
1917
            }
1918

1919
            if ($type !== '' && $nullableType === true) {
207✔
1920
                $type = '?'.$type;
51✔
1921
            }
1922
        }//end if
1923

1924
        return [
182✔
1925
            'scope'           => $scope,
273✔
1926
            'scope_specified' => $scopeSpecified,
273✔
1927
            'is_static'       => $isStatic,
273✔
1928
            'is_readonly'     => $isReadonly,
273✔
1929
            'is_final'        => $isFinal,
273✔
1930
            'type'            => $type,
273✔
1931
            'type_token'      => $typeToken,
273✔
1932
            'type_end_token'  => $typeEndToken,
273✔
1933
            'nullable_type'   => $nullableType,
273✔
1934
        ];
182✔
1935

1936
    }//end getMemberProperties()
1937

1938

1939
    /**
1940
     * Returns the visibility and implementation properties of a class.
1941
     *
1942
     * The format of the return value is:
1943
     * <code>
1944
     *   array(
1945
     *    'is_abstract' => boolean, // TRUE if the abstract keyword was found.
1946
     *    'is_final'    => boolean, // TRUE if the final keyword was found.
1947
     *    'is_readonly' => boolean, // TRUE if the readonly keyword was found.
1948
     *   );
1949
     * </code>
1950
     *
1951
     * @param int $stackPtr The position in the stack of the T_CLASS token to
1952
     *                      acquire the properties for.
1953
     *
1954
     * @return array
1955
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1956
     *                                                      T_CLASS token.
1957
     */
1958
    public function getClassProperties($stackPtr)
42✔
1959
    {
1960
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS) {
42✔
1961
            throw new RuntimeException('$stackPtr must be of type T_CLASS');
9✔
1962
        }
1963

1964
        $valid = [
22✔
1965
            T_FINAL      => T_FINAL,
33✔
1966
            T_ABSTRACT   => T_ABSTRACT,
33✔
1967
            T_READONLY   => T_READONLY,
33✔
1968
            T_WHITESPACE => T_WHITESPACE,
33✔
1969
            T_COMMENT    => T_COMMENT,
33✔
1970
        ];
22✔
1971

1972
        $isAbstract = false;
33✔
1973
        $isFinal    = false;
33✔
1974
        $isReadonly = false;
33✔
1975

1976
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
33✔
1977
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
33✔
1978
                break;
33✔
1979
            }
1980

1981
            switch ($this->tokens[$i]['code']) {
33✔
1982
            case T_ABSTRACT:
33✔
1983
                $isAbstract = true;
15✔
1984
                break;
15✔
1985

1986
            case T_FINAL:
33✔
1987
                $isFinal = true;
12✔
1988
                break;
12✔
1989

1990
            case T_READONLY:
33✔
1991
                $isReadonly = true;
15✔
1992
                break;
15✔
1993
            }
1994
        }//end for
1995

1996
        return [
22✔
1997
            'is_abstract' => $isAbstract,
33✔
1998
            'is_final'    => $isFinal,
33✔
1999
            'is_readonly' => $isReadonly,
33✔
2000
        ];
22✔
2001

2002
    }//end getClassProperties()
2003

2004

2005
    /**
2006
     * Determine if the passed token is a reference operator.
2007
     *
2008
     * Returns true if the specified token position represents a reference.
2009
     * Returns false if the token represents a bitwise operator.
2010
     *
2011
     * @param int $stackPtr The position of the T_BITWISE_AND token.
2012
     *
2013
     * @return boolean
2014
     */
2015
    public function isReference($stackPtr)
228✔
2016
    {
2017
        if ($this->tokens[$stackPtr]['code'] !== T_BITWISE_AND) {
228✔
2018
            return false;
9✔
2019
        }
2020

2021
        $tokenBefore = $this->findPrevious(
219✔
2022
            Tokens::$emptyTokens,
219✔
2023
            ($stackPtr - 1),
219✔
2024
            null,
219✔
2025
            true
219✔
2026
        );
146✔
2027

2028
        if ($this->tokens[$tokenBefore]['code'] === T_FUNCTION
219✔
2029
            || $this->tokens[$tokenBefore]['code'] === T_CLOSURE
216✔
2030
            || $this->tokens[$tokenBefore]['code'] === T_FN
219✔
2031
        ) {
2032
            // Function returns a reference.
2033
            return true;
9✔
2034
        }
2035

2036
        if ($this->tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
210✔
2037
            // Inside a foreach loop or array assignment, this is a reference.
2038
            return true;
18✔
2039
        }
2040

2041
        if ($this->tokens[$tokenBefore]['code'] === T_AS) {
192✔
2042
            // Inside a foreach loop, this is a reference.
2043
            return true;
3✔
2044
        }
2045

2046
        if (isset(Tokens::$assignmentTokens[$this->tokens[$tokenBefore]['code']]) === true) {
189✔
2047
            // This is directly after an assignment. It's a reference. Even if
2048
            // it is part of an operation, the other tests will handle it.
2049
            return true;
21✔
2050
        }
2051

2052
        $tokenAfter = $this->findNext(
168✔
2053
            Tokens::$emptyTokens,
168✔
2054
            ($stackPtr + 1),
168✔
2055
            null,
168✔
2056
            true
168✔
2057
        );
112✔
2058

2059
        if ($this->tokens[$tokenAfter]['code'] === T_NEW) {
168✔
2060
            return true;
3✔
2061
        }
2062

2063
        if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === true) {
165✔
2064
            $brackets    = $this->tokens[$stackPtr]['nested_parenthesis'];
117✔
2065
            $lastBracket = array_pop($brackets);
117✔
2066
            if (isset($this->tokens[$lastBracket]['parenthesis_owner']) === true) {
117✔
2067
                $owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']];
72✔
2068
                if ($owner['code'] === T_FUNCTION
72✔
2069
                    || $owner['code'] === T_CLOSURE
60✔
2070
                    || $owner['code'] === T_FN
72✔
2071
                ) {
2072
                    $params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']);
51✔
2073
                    foreach ($params as $param) {
65✔
2074
                        if ($param['reference_token'] === $stackPtr) {
51✔
2075
                            // Function parameter declared to be passed by reference.
2076
                            return true;
36✔
2077
                        }
2078
                    }
2079
                }//end if
2080
            } else {
2081
                $prev = false;
45✔
2082
                for ($t = ($this->tokens[$lastBracket]['parenthesis_opener'] - 1); $t >= 0; $t--) {
45✔
2083
                    if ($this->tokens[$t]['code'] !== T_WHITESPACE) {
45✔
2084
                        $prev = $t;
45✔
2085
                        break;
45✔
2086
                    }
2087
                }
2088

2089
                if ($prev !== false && $this->tokens[$prev]['code'] === T_USE) {
45✔
2090
                    // Closure use by reference.
2091
                    return true;
3✔
2092
                }
2093
            }//end if
2094
        }//end if
2095

2096
        // Pass by reference in function calls and assign by reference in arrays.
2097
        if ($this->tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
126✔
2098
            || $this->tokens[$tokenBefore]['code'] === T_COMMA
111✔
2099
            || $this->tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY
126✔
2100
        ) {
2101
            if ($this->tokens[$tokenAfter]['code'] === T_VARIABLE) {
90✔
2102
                return true;
66✔
2103
            } else {
2104
                $skip   = Tokens::$emptyTokens;
24✔
2105
                $skip[] = T_NS_SEPARATOR;
24✔
2106
                $skip[] = T_SELF;
24✔
2107
                $skip[] = T_PARENT;
24✔
2108
                $skip[] = T_STATIC;
24✔
2109
                $skip[] = T_STRING;
24✔
2110
                $skip[] = T_NAMESPACE;
24✔
2111
                $skip[] = T_DOUBLE_COLON;
24✔
2112

2113
                $nextSignificantAfter = $this->findNext(
24✔
2114
                    $skip,
24✔
2115
                    ($stackPtr + 1),
24✔
2116
                    null,
24✔
2117
                    true
24✔
2118
                );
16✔
2119
                if ($this->tokens[$nextSignificantAfter]['code'] === T_VARIABLE) {
24✔
2120
                    return true;
24✔
2121
                }
2122
            }//end if
2123
        }//end if
2124

2125
        return false;
36✔
2126

2127
    }//end isReference()
2128

2129

2130
    /**
2131
     * Returns the content of the tokens from the specified start position in
2132
     * the token stack for the specified length.
2133
     *
2134
     * @param int  $start       The position to start from in the token stack.
2135
     * @param int  $length      The length of tokens to traverse from the start pos.
2136
     * @param bool $origContent Whether the original content or the tab replaced
2137
     *                          content should be used.
2138
     *
2139
     * @return string The token contents.
2140
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position does not exist.
2141
     */
2142
    public function getTokensAsString($start, $length, $origContent=false)
84✔
2143
    {
2144
        if (is_int($start) === false || isset($this->tokens[$start]) === false) {
84✔
2145
            throw new RuntimeException('The $start position for getTokensAsString() must exist in the token stack');
6✔
2146
        }
2147

2148
        if (is_int($length) === false || $length <= 0) {
78✔
2149
            return '';
9✔
2150
        }
2151

2152
        $str = '';
69✔
2153
        $end = ($start + $length);
69✔
2154
        if ($end > $this->numTokens) {
69✔
2155
            $end = $this->numTokens;
3✔
2156
        }
2157

2158
        for ($i = $start; $i < $end; $i++) {
69✔
2159
            // If tabs are being converted to spaces by the tokeniser, the
2160
            // original content should be used instead of the converted content.
2161
            if ($origContent === true && isset($this->tokens[$i]['orig_content']) === true) {
69✔
2162
                $str .= $this->tokens[$i]['orig_content'];
6✔
2163
            } else {
2164
                $str .= $this->tokens[$i]['content'];
69✔
2165
            }
2166
        }
2167

2168
        return $str;
69✔
2169

2170
    }//end getTokensAsString()
2171

2172

2173
    /**
2174
     * Returns the position of the previous specified token(s).
2175
     *
2176
     * If a value is specified, the previous token of the specified type(s)
2177
     * containing the specified value will be returned.
2178
     *
2179
     * Returns false if no token can be found.
2180
     *
2181
     * @param int|string|array $types   The type(s) of tokens to search for.
2182
     * @param int              $start   The position to start searching from in the
2183
     *                                  token stack.
2184
     * @param int|null         $end     The end position to fail if no token is found.
2185
     *                                  if not specified or null, end will default to
2186
     *                                  the start of the token stack.
2187
     * @param bool             $exclude If true, find the previous token that is NOT of
2188
     *                                  the types specified in $types.
2189
     * @param string|null      $value   The value that the token(s) must be equal to.
2190
     *                                  If value is omitted, tokens with any value will
2191
     *                                  be returned.
2192
     * @param bool             $local   If true, tokens outside the current statement
2193
     *                                  will not be checked. IE. checking will stop
2194
     *                                  at the previous semicolon found.
2195
     *
2196
     * @return int|false
2197
     * @see    findNext()
2198
     */
2199
    public function findPrevious(
×
2200
        $types,
2201
        $start,
2202
        $end=null,
2203
        $exclude=false,
2204
        $value=null,
2205
        $local=false
2206
    ) {
2207
        $types = (array) $types;
×
2208

2209
        if ($end === null) {
×
2210
            $end = 0;
×
2211
        }
2212

2213
        for ($i = $start; $i >= $end; $i--) {
×
2214
            $found = (bool) $exclude;
×
2215
            foreach ($types as $type) {
×
2216
                if ($this->tokens[$i]['code'] === $type) {
×
2217
                    $found = !$exclude;
×
2218
                    break;
×
2219
                }
2220
            }
2221

2222
            if ($found === true) {
×
2223
                if ($value === null) {
×
2224
                    return $i;
×
2225
                } else if ($this->tokens[$i]['content'] === $value) {
×
2226
                    return $i;
×
2227
                }
2228
            }
2229

2230
            if ($local === true) {
×
2231
                if (isset($this->tokens[$i]['scope_opener']) === true
×
2232
                    && $i === $this->tokens[$i]['scope_closer']
×
2233
                ) {
2234
                    $i = $this->tokens[$i]['scope_opener'];
×
2235
                } else if (isset($this->tokens[$i]['bracket_opener']) === true
×
2236
                    && $i === $this->tokens[$i]['bracket_closer']
×
2237
                ) {
2238
                    $i = $this->tokens[$i]['bracket_opener'];
×
2239
                } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
×
2240
                    && $i === $this->tokens[$i]['parenthesis_closer']
×
2241
                ) {
2242
                    $i = $this->tokens[$i]['parenthesis_opener'];
×
2243
                } else if ($this->tokens[$i]['code'] === T_SEMICOLON) {
×
2244
                    break;
×
2245
                }
2246
            }
2247
        }//end for
2248

2249
        return false;
×
2250

2251
    }//end findPrevious()
2252

2253

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

2290
        if ($end === null || $end > $this->numTokens) {
×
2291
            $end = $this->numTokens;
×
2292
        }
2293

2294
        for ($i = $start; $i < $end; $i++) {
×
2295
            $found = (bool) $exclude;
×
2296
            foreach ($types as $type) {
×
2297
                if ($this->tokens[$i]['code'] === $type) {
×
2298
                    $found = !$exclude;
×
2299
                    break;
×
2300
                }
2301
            }
2302

2303
            if ($found === true) {
×
2304
                if ($value === null) {
×
2305
                    return $i;
×
2306
                } else if ($this->tokens[$i]['content'] === $value) {
×
2307
                    return $i;
×
2308
                }
2309
            }
2310

2311
            if ($local === true && $this->tokens[$i]['code'] === T_SEMICOLON) {
×
2312
                break;
×
2313
            }
2314
        }//end for
2315

2316
        return false;
×
2317

2318
    }//end findNext()
2319

2320

2321
    /**
2322
     * Returns the position of the first non-whitespace token in a statement.
2323
     *
2324
     * @param int              $start  The position to start searching from in the token stack.
2325
     * @param int|string|array $ignore Token types that should not be considered stop points.
2326
     *
2327
     * @return int
2328
     */
2329
    public function findStartOfStatement($start, $ignore=null)
210✔
2330
    {
2331
        $startTokens = Tokens::$blockOpeners;
210✔
2332
        $startTokens[T_OPEN_SHORT_ARRAY]   = true;
210✔
2333
        $startTokens[T_OPEN_TAG]           = true;
210✔
2334
        $startTokens[T_OPEN_TAG_WITH_ECHO] = true;
210✔
2335

2336
        $endTokens = [
140✔
2337
            T_CLOSE_TAG    => true,
210✔
2338
            T_COLON        => true,
210✔
2339
            T_COMMA        => true,
210✔
2340
            T_DOUBLE_ARROW => true,
210✔
2341
            T_MATCH_ARROW  => true,
210✔
2342
            T_SEMICOLON    => true,
210✔
2343
        ];
140✔
2344

2345
        if ($ignore !== null) {
210✔
2346
            $ignore = (array) $ignore;
×
2347
            foreach ($ignore as $code) {
×
2348
                if (isset($startTokens[$code]) === true) {
×
2349
                    unset($startTokens[$code]);
×
2350
                }
2351

2352
                if (isset($endTokens[$code]) === true) {
×
2353
                    unset($endTokens[$code]);
×
2354
                }
2355
            }
2356
        }
2357

2358
        // If the start token is inside the case part of a match expression,
2359
        // find the start of the condition. If it's in the statement part, find
2360
        // the token that comes after the match arrow.
2361
        if (empty($this->tokens[$start]['conditions']) === false) {
210✔
2362
            $conditions         = $this->tokens[$start]['conditions'];
162✔
2363
            $lastConditionOwner = end($conditions);
162✔
2364
            $matchExpression    = key($conditions);
162✔
2365

2366
            if ($lastConditionOwner === T_MATCH
162✔
2367
                // Check if the $start token is at the same parentheses nesting level as the match token.
2368
                && ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === true
139✔
2369
                && empty($this->tokens[$start]['nested_parenthesis']) === true)
131✔
2370
                || ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === false
125✔
2371
                && empty($this->tokens[$start]['nested_parenthesis']) === false)
125✔
2372
                && $this->tokens[$matchExpression]['nested_parenthesis'] === $this->tokens[$start]['nested_parenthesis']))
162✔
2373
            ) {
2374
                // Walk back to the previous match arrow (if it exists).
2375
                $lastComma          = null;
45✔
2376
                $inNestedExpression = false;
45✔
2377
                for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
45✔
2378
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_MATCH_ARROW) {
45✔
2379
                        break;
33✔
2380
                    }
2381

2382
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_COMMA) {
45✔
2383
                        $lastComma = $prevMatch;
24✔
2384
                        continue;
24✔
2385
                    }
2386

2387
                    // Skip nested statements.
2388
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
45✔
2389
                        && $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
45✔
2390
                    ) {
2391
                        $prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
12✔
2392
                        continue;
12✔
2393
                    }
2394

2395
                    if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
45✔
2396
                        && $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
45✔
2397
                    ) {
2398
                        $prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
15✔
2399
                        continue;
15✔
2400
                    }
2401

2402
                    // Stop if we're _within_ a nested short array statement, which may contain comma's too.
2403
                    // No need to deal with parentheses, those are handled above via the `nested_parenthesis` checks.
2404
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
45✔
2405
                        && $this->tokens[$prevMatch]['bracket_closer'] > $start
45✔
2406
                    ) {
2407
                        $inNestedExpression = true;
15✔
2408
                        break;
15✔
2409
                    }
2410
                }//end for
2411

2412
                if ($inNestedExpression === false) {
45✔
2413
                    // $prevMatch will now either be the scope opener or a match arrow.
2414
                    // If it is the scope opener, go the first non-empty token after. $start will have been part of the first condition.
2415
                    if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) {
33✔
2416
                        // We're before the arrow in the first case.
2417
                        $next = $this->findNext(Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
12✔
2418
                        if ($next === false) {
12✔
2419
                            // Shouldn't be possible.
2420
                            return $start;
×
2421
                        }
2422

2423
                        return $next;
12✔
2424
                    }
2425

2426
                    // Okay, so we found a match arrow.
2427
                    // If $start was part of the "next" condition, the last comma will be set.
2428
                    // Otherwise, $start must have been part of a return expression.
2429
                    if (isset($lastComma) === true && $lastComma > $prevMatch) {
33✔
2430
                        $prevMatch = $lastComma;
15✔
2431
                    }
2432

2433
                    // In both cases, go to the first non-empty token after.
2434
                    $next = $this->findNext(Tokens::$emptyTokens, ($prevMatch + 1), null, true);
33✔
2435
                    if ($next === false) {
33✔
2436
                        // Shouldn't be possible.
2437
                        return $start;
×
2438
                    }
2439

2440
                    return $next;
33✔
2441
                }//end if
2442
            }//end if
2443
        }//end if
2444

2445
        $lastNotEmpty = $start;
180✔
2446

2447
        // If we are starting at a token that ends a scope block, skip to
2448
        // the start and continue from there.
2449
        // If we are starting at a token that ends a statement, skip this
2450
        // token so we find the true start of the statement.
2451
        while (isset($endTokens[$this->tokens[$start]['code']]) === true
180✔
2452
            || (isset($this->tokens[$start]['scope_condition']) === true
180✔
2453
            && $start === $this->tokens[$start]['scope_closer'])
180✔
2454
        ) {
2455
            if (isset($this->tokens[$start]['scope_condition']) === true) {
51✔
2456
                $start = $this->tokens[$start]['scope_condition'];
27✔
2457
            } else {
2458
                $start--;
30✔
2459
            }
2460
        }
2461

2462
        for ($i = $start; $i >= 0; $i--) {
180✔
2463
            if (isset($startTokens[$this->tokens[$i]['code']]) === true
180✔
2464
                || isset($endTokens[$this->tokens[$i]['code']]) === true
180✔
2465
            ) {
2466
                // Found the end of the previous statement.
2467
                return $lastNotEmpty;
180✔
2468
            }
2469

2470
            if (isset($this->tokens[$i]['scope_opener']) === true
177✔
2471
                && $i === $this->tokens[$i]['scope_closer']
177✔
2472
                && $this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS
177✔
2473
                && $this->tokens[$i]['code'] !== T_END_NOWDOC
177✔
2474
                && $this->tokens[$i]['code'] !== T_END_HEREDOC
177✔
2475
                && $this->tokens[$i]['code'] !== T_BREAK
177✔
2476
                && $this->tokens[$i]['code'] !== T_RETURN
177✔
2477
                && $this->tokens[$i]['code'] !== T_CONTINUE
177✔
2478
                && $this->tokens[$i]['code'] !== T_THROW
177✔
2479
                && $this->tokens[$i]['code'] !== T_EXIT
177✔
2480
                && $this->tokens[$i]['code'] !== T_GOTO
177✔
2481
            ) {
2482
                // Found the end of the previous scope block.
2483
                return $lastNotEmpty;
3✔
2484
            }
2485

2486
            // Skip nested statements.
2487
            if (isset($this->tokens[$i]['bracket_opener']) === true
177✔
2488
                && $i === $this->tokens[$i]['bracket_closer']
177✔
2489
            ) {
2490
                $i = $this->tokens[$i]['bracket_opener'];
3✔
2491
            } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
177✔
2492
                && $i === $this->tokens[$i]['parenthesis_closer']
177✔
2493
            ) {
2494
                $i = $this->tokens[$i]['parenthesis_opener'];
21✔
2495
            } else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
177✔
2496
                $start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
6✔
2497
                if ($start !== false) {
6✔
2498
                    $i = $start;
6✔
2499
                }
2500
            }//end if
2501

2502
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
177✔
2503
                $lastNotEmpty = $i;
177✔
2504
            }
2505
        }//end for
2506

2507
        return 0;
×
2508

2509
    }//end findStartOfStatement()
2510

2511

2512
    /**
2513
     * Returns the position of the last non-whitespace token in a statement.
2514
     *
2515
     * @param int              $start  The position to start searching from in the token stack.
2516
     * @param int|string|array $ignore Token types that should not be considered stop points.
2517
     *
2518
     * @return int
2519
     */
2520
    public function findEndOfStatement($start, $ignore=null)
66✔
2521
    {
2522
        $endTokens = [
44✔
2523
            T_COLON                => true,
66✔
2524
            T_COMMA                => true,
66✔
2525
            T_DOUBLE_ARROW         => true,
66✔
2526
            T_SEMICOLON            => true,
66✔
2527
            T_CLOSE_PARENTHESIS    => true,
66✔
2528
            T_CLOSE_SQUARE_BRACKET => true,
66✔
2529
            T_CLOSE_CURLY_BRACKET  => true,
66✔
2530
            T_CLOSE_SHORT_ARRAY    => true,
66✔
2531
            T_OPEN_TAG             => true,
66✔
2532
            T_CLOSE_TAG            => true,
66✔
2533
        ];
44✔
2534

2535
        if ($ignore !== null) {
66✔
2536
            $ignore = (array) $ignore;
×
2537
            foreach ($ignore as $code) {
×
2538
                unset($endTokens[$code]);
×
2539
            }
2540
        }
2541

2542
        // If the start token is inside the case part of a match expression,
2543
        // advance to the match arrow and continue looking for the
2544
        // end of the statement from there so that we skip over commas.
2545
        if ($this->tokens[$start]['code'] !== T_MATCH_ARROW) {
66✔
2546
            $matchExpression = $this->getCondition($start, T_MATCH);
66✔
2547
            if ($matchExpression !== false) {
66✔
2548
                $beforeArrow    = true;
30✔
2549
                $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']);
30✔
2550
                if ($prevMatchArrow !== false) {
30✔
2551
                    $prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start);
27✔
2552
                    if ($prevComma === false) {
27✔
2553
                        // No comma between this token and the last match arrow,
2554
                        // so this token exists after the arrow and we can continue
2555
                        // checking as normal.
2556
                        $beforeArrow = false;
12✔
2557
                    }
2558
                }
2559

2560
                if ($beforeArrow === true) {
30✔
2561
                    $nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
30✔
2562
                    if ($nextMatchArrow !== false) {
30✔
2563
                        $start = $nextMatchArrow;
30✔
2564
                    }
2565
                }
2566
            }//end if
2567
        }//end if
2568

2569
        $lastNotEmpty = $start;
66✔
2570
        for ($i = $start; $i < $this->numTokens; $i++) {
66✔
2571
            if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
66✔
2572
                // Found the end of the statement.
2573
                if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS
60✔
2574
                    || $this->tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
57✔
2575
                    || $this->tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
57✔
2576
                    || $this->tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
51✔
2577
                    || $this->tokens[$i]['code'] === T_OPEN_TAG
48✔
2578
                    || $this->tokens[$i]['code'] === T_CLOSE_TAG
60✔
2579
                ) {
2580
                    return $lastNotEmpty;
24✔
2581
                }
2582

2583
                return $i;
48✔
2584
            }
2585

2586
            // Skip nested statements.
2587
            if (isset($this->tokens[$i]['scope_closer']) === true
66✔
2588
                && ($i === $this->tokens[$i]['scope_opener']
56✔
2589
                || $i === $this->tokens[$i]['scope_condition'])
66✔
2590
            ) {
2591
                if ($this->tokens[$i]['code'] === T_FN) {
36✔
2592
                    $lastNotEmpty = $this->tokens[$i]['scope_closer'];
18✔
2593
                    $i            = ($this->tokens[$i]['scope_closer'] - 1);
18✔
2594
                    continue;
18✔
2595
                }
2596

2597
                if ($i === $start && isset(Tokens::$scopeOpeners[$this->tokens[$i]['code']]) === true) {
21✔
2598
                    return $this->tokens[$i]['scope_closer'];
9✔
2599
                }
2600

2601
                $i = $this->tokens[$i]['scope_closer'];
15✔
2602
            } else if (isset($this->tokens[$i]['bracket_closer']) === true
48✔
2603
                && $i === $this->tokens[$i]['bracket_opener']
48✔
2604
            ) {
2605
                $i = $this->tokens[$i]['bracket_closer'];
6✔
2606
            } else if (isset($this->tokens[$i]['parenthesis_closer']) === true
48✔
2607
                && $i === $this->tokens[$i]['parenthesis_opener']
48✔
2608
            ) {
2609
                $i = $this->tokens[$i]['parenthesis_closer'];
9✔
2610
            } else if ($this->tokens[$i]['code'] === T_OPEN_USE_GROUP) {
48✔
2611
                $end = $this->findNext(T_CLOSE_USE_GROUP, ($i + 1));
6✔
2612
                if ($end !== false) {
6✔
2613
                    $i = $end;
6✔
2614
                }
2615
            }//end if
2616

2617
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
48✔
2618
                $lastNotEmpty = $i;
48✔
2619
            }
2620
        }//end for
2621

2622
        return ($this->numTokens - 1);
3✔
2623

2624
    }//end findEndOfStatement()
2625

2626

2627
    /**
2628
     * Returns the position of the first token on a line, matching given type.
2629
     *
2630
     * Returns false if no token can be found.
2631
     *
2632
     * @param int|string|array $types   The type(s) of tokens to search for.
2633
     * @param int              $start   The position to start searching from in the
2634
     *                                  token stack.
2635
     * @param bool             $exclude If true, find the token that is NOT of
2636
     *                                  the types specified in $types.
2637
     * @param string           $value   The value that the token must be equal to.
2638
     *                                  If value is omitted, tokens with any value will
2639
     *                                  be returned.
2640
     *
2641
     * @return int|false The first token which matches on the line containing the start
2642
     *                   token, between the start of the line and the start token.
2643
     *                   Note: The first token matching might be the start token.
2644
     *                   FALSE when no matching token could be found between the start of
2645
     *                   the line and the start token.
2646
     */
2647
    public function findFirstOnLine($types, $start, $exclude=false, $value=null)
×
2648
    {
2649
        if (is_array($types) === false) {
×
2650
            $types = [$types];
×
2651
        }
2652

2653
        $foundToken = false;
×
2654

2655
        for ($i = $start; $i >= 0; $i--) {
×
2656
            if ($this->tokens[$i]['line'] < $this->tokens[$start]['line']) {
×
2657
                break;
×
2658
            }
2659

2660
            $found = $exclude;
×
2661
            foreach ($types as $type) {
×
2662
                if ($exclude === false) {
×
2663
                    if ($this->tokens[$i]['code'] === $type) {
×
2664
                        $found = true;
×
2665
                        break;
×
2666
                    }
2667
                } else {
2668
                    if ($this->tokens[$i]['code'] === $type) {
×
2669
                        $found = false;
×
2670
                        break;
×
2671
                    }
2672
                }
2673
            }
2674

2675
            if ($found === true) {
×
2676
                if ($value === null) {
×
2677
                    $foundToken = $i;
×
2678
                } else if ($this->tokens[$i]['content'] === $value) {
×
2679
                    $foundToken = $i;
×
2680
                }
2681
            }
2682
        }//end for
2683

2684
        return $foundToken;
×
2685

2686
    }//end findFirstOnLine()
2687

2688

2689
    /**
2690
     * Determine if the passed token has a condition of one of the passed types.
2691
     *
2692
     * @param int              $stackPtr The position of the token we are checking.
2693
     * @param int|string|array $types    The type(s) of tokens to search for.
2694
     *
2695
     * @return boolean
2696
     */
2697
    public function hasCondition($stackPtr, $types)
21✔
2698
    {
2699
        // Check for the existence of the token.
2700
        if (isset($this->tokens[$stackPtr]) === false) {
21✔
2701
            return false;
3✔
2702
        }
2703

2704
        // Make sure the token has conditions.
2705
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
18✔
2706
            return false;
3✔
2707
        }
2708

2709
        $types      = (array) $types;
15✔
2710
        $conditions = $this->tokens[$stackPtr]['conditions'];
15✔
2711

2712
        foreach ($types as $type) {
15✔
2713
            if (in_array($type, $conditions, true) === true) {
15✔
2714
                // We found a token with the required type.
2715
                return true;
15✔
2716
            }
2717
        }
2718

2719
        return false;
15✔
2720

2721
    }//end hasCondition()
2722

2723

2724
    /**
2725
     * Return the position of the condition for the passed token.
2726
     *
2727
     * Returns FALSE if the token does not have the condition.
2728
     *
2729
     * @param int        $stackPtr The position of the token we are checking.
2730
     * @param int|string $type     The type of token to search for.
2731
     * @param bool       $first    If TRUE, will return the matched condition
2732
     *                             furthest away from the passed token.
2733
     *                             If FALSE, will return the matched condition
2734
     *                             closest to the passed token.
2735
     *
2736
     * @return int|false
2737
     */
2738
    public function getCondition($stackPtr, $type, $first=true)
30✔
2739
    {
2740
        // Check for the existence of the token.
2741
        if (isset($this->tokens[$stackPtr]) === false) {
30✔
2742
            return false;
3✔
2743
        }
2744

2745
        // Make sure the token has conditions.
2746
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
27✔
2747
            return false;
3✔
2748
        }
2749

2750
        $conditions = $this->tokens[$stackPtr]['conditions'];
24✔
2751
        if ($first === false) {
24✔
2752
            $conditions = array_reverse($conditions, true);
12✔
2753
        }
2754

2755
        foreach ($conditions as $token => $condition) {
24✔
2756
            if ($condition === $type) {
24✔
2757
                return $token;
24✔
2758
            }
2759
        }
2760

2761
        return false;
24✔
2762

2763
    }//end getCondition()
2764

2765

2766
    /**
2767
     * Returns the name of the class that the specified class extends.
2768
     * (works for classes, anonymous classes and interfaces)
2769
     *
2770
     * Returns FALSE on error or if there is no extended class name.
2771
     *
2772
     * @param int $stackPtr The stack position of the class.
2773
     *
2774
     * @return string|false
2775
     */
2776
    public function findExtendedClassName($stackPtr)
51✔
2777
    {
2778
        // Check for the existence of the token.
2779
        if (isset($this->tokens[$stackPtr]) === false) {
51✔
2780
            return false;
3✔
2781
        }
2782

2783
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
48✔
2784
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
48✔
2785
            && $this->tokens[$stackPtr]['code'] !== T_INTERFACE
48✔
2786
        ) {
2787
            return false;
3✔
2788
        }
2789

2790
        if (isset($this->tokens[$stackPtr]['scope_opener']) === false) {
45✔
2791
            return false;
3✔
2792
        }
2793

2794
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
42✔
2795
        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex);
42✔
2796
        if ($extendsIndex === false) {
42✔
2797
            return false;
9✔
2798
        }
2799

2800
        $find = [
22✔
2801
            T_NS_SEPARATOR,
33✔
2802
            T_STRING,
33✔
2803
            T_WHITESPACE,
33✔
2804
        ];
22✔
2805

2806
        $end  = $this->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true);
33✔
2807
        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
33✔
2808
        $name = trim($name);
33✔
2809

2810
        if ($name === '') {
33✔
2811
            return false;
3✔
2812
        }
2813

2814
        return $name;
30✔
2815

2816
    }//end findExtendedClassName()
2817

2818

2819
    /**
2820
     * Returns the names of the interfaces that the specified class or enum implements.
2821
     *
2822
     * Returns FALSE on error or if there are no implemented interface names.
2823
     *
2824
     * @param int $stackPtr The stack position of the class or enum token.
2825
     *
2826
     * @return array|false
2827
     */
2828
    public function findImplementedInterfaceNames($stackPtr)
48✔
2829
    {
2830
        // Check for the existence of the token.
2831
        if (isset($this->tokens[$stackPtr]) === false) {
48✔
2832
            return false;
3✔
2833
        }
2834

2835
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
45✔
2836
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
45✔
2837
            && $this->tokens[$stackPtr]['code'] !== T_ENUM
45✔
2838
        ) {
2839
            return false;
6✔
2840
        }
2841

2842
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
39✔
2843
            return false;
3✔
2844
        }
2845

2846
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
36✔
2847
        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
36✔
2848
        if ($implementsIndex === false) {
36✔
2849
            return false;
6✔
2850
        }
2851

2852
        $find = [
20✔
2853
            T_NS_SEPARATOR,
30✔
2854
            T_STRING,
30✔
2855
            T_WHITESPACE,
30✔
2856
            T_COMMA,
30✔
2857
        ];
20✔
2858

2859
        $end  = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
30✔
2860
        $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
30✔
2861
        $name = trim($name);
30✔
2862

2863
        if ($name === '') {
30✔
2864
            return false;
3✔
2865
        } else {
2866
            $names = explode(',', $name);
27✔
2867
            $names = array_map('trim', $names);
27✔
2868
            return $names;
27✔
2869
        }
2870

2871
    }//end findImplementedInterfaceNames()
2872

2873

2874
}//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

© 2026 Coveralls, Inc