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

PHPCSStandards / PHP_CodeSniffer / 14523511255

17 Apr 2025 07:39PM UTC coverage: 77.921% (-0.1%) from 78.02%
14523511255

Pull #1020

github

web-flow
Merge 121c58cd4 into f78bb64bd
Pull Request #1020: Tokenizer/PHP: namespaced names as single token, mirroring PHP 8.0+

203 of 211 new or added lines in 25 files covered. (96.21%)

6 existing lines in 3 files now uncovered.

19389 of 24883 relevant lines covered (77.92%)

85.62 hits per line

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

70.08
/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) {
×
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']);
×
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) {
×
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);
×
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) {
×
497
            StatusWriter::write('*** END TOKEN PROCESSING ***', 1);
×
498
            StatusWriter::write('*** START SNIFF PROCESSING REPORT ***', 1);
×
499

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

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) {
×
532
                $newlines = 0;
×
533
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
534
                    $newlines = 1;
×
535
                }
536

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

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

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();
×
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'];
35✔
1236
        } else if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
34✔
1237
            // For OO tokens, stop searching at the open curly.
1238
            $stopPoint = $this->tokens[$stackPtr]['scope_opener'];
31✔
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)
237✔
1307
    {
1308
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
237✔
1309
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
237✔
1310
            && $this->tokens[$stackPtr]['code'] !== T_USE
237✔
1311
            && $this->tokens[$stackPtr]['code'] !== T_FN
237✔
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
228✔
1317
            && isset($this->tokens[$stackPtr]['parenthesis_owner']) === false
228✔
1318
        ) {
1319
            throw new RuntimeException('$stackPtr was not a valid T_USE');
12✔
1320
        }
1321

1322
        if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === false) {
216✔
1323
            // Live coding or syntax error, so no params to find.
1324
            return [];
3✔
1325
        }
1326

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

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

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

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

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

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

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

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

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

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

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

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

1445
                if ($defaultStart === null) {
135✔
1446
                    if ($typeHintToken === false) {
132✔
1447
                        $typeHintToken = $i;
117✔
1448
                    }
1449

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

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

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

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

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

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

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

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

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

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

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

1569
        return $vars;
207✔
1570

1571
    }//end getMethodParameters()
1572

1573

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

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

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

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

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

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

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

1679
            $valid  = Tokens::$nameTokens;
168✔
1680
            $valid += [
112✔
1681
                T_CALLABLE               => T_CALLABLE,
168✔
1682
                T_SELF                   => T_SELF,
168✔
1683
                T_PARENT                 => T_PARENT,
168✔
1684
                T_STATIC                 => T_STATIC,
168✔
1685
                T_FALSE                  => T_FALSE,
168✔
1686
                T_TRUE                   => T_TRUE,
168✔
1687
                T_NULL                   => T_NULL,
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
                // Skip over closure use statements.
1703
                if ($this->tokens[$i]['code'] === T_USE) {
168✔
1704
                    if (isset($this->tokens[$i]['parenthesis_closer']) === false) {
15✔
1705
                        // Live coding/parse error, stop parsing.
1706
                        break;
×
1707
                    }
1708

1709
                    $i = $this->tokens[$i]['parenthesis_closer'];
15✔
1710
                    continue;
15✔
1711
                }
1712

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

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

1722
                    $returnType        .= $this->tokens[$i]['content'];
144✔
1723
                    $returnTypeEndToken = $i;
144✔
1724
                }
1725
            }//end for
1726

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

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

1737
        if ($returnType !== '' && $nullableReturnType === true) {
168✔
1738
            $returnType = '?'.$returnType;
39✔
1739
        }
1740

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

1754
    }//end getMethodProperties()
1755

1756

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

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

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

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

1825
        $valid += Tokens::$emptyTokens;
273✔
1826

1827
        $scope          = 'public';
273✔
1828
        $scopeSpecified = false;
273✔
1829
        $isStatic       = false;
273✔
1830
        $isReadonly     = false;
273✔
1831
        $isFinal        = false;
273✔
1832

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

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

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

1873
        $type         = '';
273✔
1874
        $typeToken    = false;
273✔
1875
        $typeEndToken = false;
273✔
1876
        $nullableType = false;
273✔
1877

1878
        if ($i < $stackPtr) {
273✔
1879
            // We've found a type.
1880
            $valid  = Tokens::$nameTokens;
207✔
1881
            $valid += [
138✔
1882
                T_CALLABLE               => T_CALLABLE,
207✔
1883
                T_SELF                   => T_SELF,
207✔
1884
                T_PARENT                 => T_PARENT,
207✔
1885
                T_FALSE                  => T_FALSE,
207✔
1886
                T_TRUE                   => T_TRUE,
207✔
1887
                T_NULL                   => T_NULL,
207✔
1888
                T_TYPE_UNION             => T_TYPE_UNION,
207✔
1889
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
207✔
1890
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
207✔
1891
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
207✔
1892
            ];
138✔
1893

1894
            for ($i; $i < $stackPtr; $i++) {
207✔
1895
                if ($this->tokens[$i]['code'] === T_VARIABLE) {
207✔
1896
                    // Hit another variable in a group definition.
1897
                    break;
30✔
1898
                }
1899

1900
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
183✔
1901
                    $nullableType = true;
51✔
1902
                }
1903

1904
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
183✔
1905
                    $typeEndToken = $i;
183✔
1906
                    if ($typeToken === false) {
183✔
1907
                        $typeToken = $i;
183✔
1908
                    }
1909

1910
                    $type .= $this->tokens[$i]['content'];
183✔
1911
                }
1912
            }
1913

1914
            if ($type !== '' && $nullableType === true) {
207✔
1915
                $type = '?'.$type;
51✔
1916
            }
1917
        }//end if
1918

1919
        return [
182✔
1920
            'scope'           => $scope,
273✔
1921
            'scope_specified' => $scopeSpecified,
273✔
1922
            'is_static'       => $isStatic,
273✔
1923
            'is_readonly'     => $isReadonly,
273✔
1924
            'is_final'        => $isFinal,
273✔
1925
            'type'            => $type,
273✔
1926
            'type_token'      => $typeToken,
273✔
1927
            'type_end_token'  => $typeEndToken,
273✔
1928
            'nullable_type'   => $nullableType,
273✔
1929
        ];
182✔
1930

1931
    }//end getMemberProperties()
1932

1933

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

1959
        $valid = [
22✔
1960
            T_FINAL      => T_FINAL,
33✔
1961
            T_ABSTRACT   => T_ABSTRACT,
33✔
1962
            T_READONLY   => T_READONLY,
33✔
1963
            T_WHITESPACE => T_WHITESPACE,
33✔
1964
            T_COMMENT    => T_COMMENT,
33✔
1965
        ];
22✔
1966

1967
        $isAbstract = false;
33✔
1968
        $isFinal    = false;
33✔
1969
        $isReadonly = false;
33✔
1970

1971
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
33✔
1972
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
33✔
1973
                break;
33✔
1974
            }
1975

1976
            switch ($this->tokens[$i]['code']) {
33✔
1977
            case T_ABSTRACT:
33✔
1978
                $isAbstract = true;
15✔
1979
                break;
15✔
1980

1981
            case T_FINAL:
33✔
1982
                $isFinal = true;
12✔
1983
                break;
12✔
1984

1985
            case T_READONLY:
33✔
1986
                $isReadonly = true;
15✔
1987
                break;
15✔
1988
            }
1989
        }//end for
1990

1991
        return [
22✔
1992
            'is_abstract' => $isAbstract,
33✔
1993
            'is_final'    => $isFinal,
33✔
1994
            'is_readonly' => $isReadonly,
33✔
1995
        ];
22✔
1996

1997
    }//end getClassProperties()
1998

1999

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

2016
        $tokenBefore = $this->findPrevious(
219✔
2017
            Tokens::$emptyTokens,
219✔
2018
            ($stackPtr - 1),
219✔
2019
            null,
219✔
2020
            true
219✔
2021
        );
146✔
2022

2023
        if ($this->tokens[$tokenBefore]['code'] === T_FUNCTION
219✔
2024
            || $this->tokens[$tokenBefore]['code'] === T_CLOSURE
216✔
2025
            || $this->tokens[$tokenBefore]['code'] === T_FN
219✔
2026
        ) {
2027
            // Function returns a reference.
2028
            return true;
9✔
2029
        }
2030

2031
        if ($this->tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
210✔
2032
            // Inside a foreach loop or array assignment, this is a reference.
2033
            return true;
18✔
2034
        }
2035

2036
        if ($this->tokens[$tokenBefore]['code'] === T_AS) {
192✔
2037
            // Inside a foreach loop, this is a reference.
2038
            return true;
3✔
2039
        }
2040

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

2047
        $tokenAfter = $this->findNext(
168✔
2048
            Tokens::$emptyTokens,
168✔
2049
            ($stackPtr + 1),
168✔
2050
            null,
168✔
2051
            true
168✔
2052
        );
112✔
2053

2054
        if ($this->tokens[$tokenAfter]['code'] === T_NEW) {
168✔
2055
            return true;
3✔
2056
        }
2057

2058
        if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === true) {
165✔
2059
            $brackets    = $this->tokens[$stackPtr]['nested_parenthesis'];
117✔
2060
            $lastBracket = array_pop($brackets);
117✔
2061
            if (isset($this->tokens[$lastBracket]['parenthesis_owner']) === true) {
117✔
2062
                $owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']];
81✔
2063
                if ($owner['code'] === T_FUNCTION
81✔
2064
                    || $owner['code'] === T_CLOSURE
69✔
2065
                    || $owner['code'] === T_FN
42✔
2066
                    || $owner['code'] === T_USE
81✔
2067
                ) {
2068
                    $params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']);
60✔
2069
                    foreach ($params as $param) {
60✔
2070
                        if ($param['reference_token'] === $stackPtr) {
60✔
2071
                            // Function parameter declared to be passed by reference.
2072
                            return true;
45✔
2073
                        }
2074
                    }
2075
                }//end if
2076
            }//end if
2077
        }//end if
2078

2079
        // Pass by reference in function calls and assign by reference in arrays.
2080
        if ($this->tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
120✔
2081
            || $this->tokens[$tokenBefore]['code'] === T_COMMA
108✔
2082
            || $this->tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY
120✔
2083
        ) {
2084
            if ($this->tokens[$tokenAfter]['code'] === T_VARIABLE) {
84✔
2085
                return true;
60✔
2086
            } else {
2087
                $skip   = Tokens::$emptyTokens;
24✔
2088
                $skip  += Tokens::$nameTokens;
24✔
2089
                $skip[] = T_SELF;
24✔
2090
                $skip[] = T_PARENT;
24✔
2091
                $skip[] = T_STATIC;
24✔
2092
                $skip[] = T_DOUBLE_COLON;
24✔
2093

2094
                $nextSignificantAfter = $this->findNext(
24✔
2095
                    $skip,
24✔
2096
                    ($stackPtr + 1),
24✔
2097
                    null,
24✔
2098
                    true
24✔
2099
                );
16✔
2100
                if ($this->tokens[$nextSignificantAfter]['code'] === T_VARIABLE) {
24✔
2101
                    return true;
24✔
2102
                }
2103
            }//end if
2104
        }//end if
2105

2106
        return false;
36✔
2107

2108
    }//end isReference()
2109

2110

2111
    /**
2112
     * Returns the content of the tokens from the specified start position in
2113
     * the token stack for the specified length.
2114
     *
2115
     * @param int  $start       The position to start from in the token stack.
2116
     * @param int  $length      The length of tokens to traverse from the start pos.
2117
     * @param bool $origContent Whether the original content or the tab replaced
2118
     *                          content should be used.
2119
     *
2120
     * @return string The token contents.
2121
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position does not exist.
2122
     */
2123
    public function getTokensAsString($start, $length, $origContent=false)
84✔
2124
    {
2125
        if (is_int($start) === false || isset($this->tokens[$start]) === false) {
84✔
2126
            throw new RuntimeException('The $start position for getTokensAsString() must exist in the token stack');
6✔
2127
        }
2128

2129
        if (is_int($length) === false || $length <= 0) {
78✔
2130
            return '';
9✔
2131
        }
2132

2133
        $str = '';
69✔
2134
        $end = ($start + $length);
69✔
2135
        if ($end > $this->numTokens) {
69✔
2136
            $end = $this->numTokens;
3✔
2137
        }
2138

2139
        for ($i = $start; $i < $end; $i++) {
69✔
2140
            // If tabs are being converted to spaces by the tokeniser, the
2141
            // original content should be used instead of the converted content.
2142
            if ($origContent === true && isset($this->tokens[$i]['orig_content']) === true) {
69✔
2143
                $str .= $this->tokens[$i]['orig_content'];
6✔
2144
            } else {
2145
                $str .= $this->tokens[$i]['content'];
69✔
2146
            }
2147
        }
2148

2149
        return $str;
69✔
2150

2151
    }//end getTokensAsString()
2152

2153

2154
    /**
2155
     * Returns the position of the previous specified token(s).
2156
     *
2157
     * If a value is specified, the previous token of the specified type(s)
2158
     * containing the specified value will be returned.
2159
     *
2160
     * Returns false if no token can be found.
2161
     *
2162
     * @param int|string|array $types   The type(s) of tokens to search for.
2163
     * @param int              $start   The position to start searching from in the
2164
     *                                  token stack.
2165
     * @param int|null         $end     The end position to fail if no token is found.
2166
     *                                  if not specified or null, end will default to
2167
     *                                  the start of the token stack.
2168
     * @param bool             $exclude If true, find the previous token that is NOT of
2169
     *                                  the types specified in $types.
2170
     * @param string|null      $value   The value that the token(s) must be equal to.
2171
     *                                  If value is omitted, tokens with any value will
2172
     *                                  be returned.
2173
     * @param bool             $local   If true, tokens outside the current statement
2174
     *                                  will not be checked. IE. checking will stop
2175
     *                                  at the previous semicolon found.
2176
     *
2177
     * @return int|false
2178
     * @see    findNext()
2179
     */
2180
    public function findPrevious(
×
2181
        $types,
2182
        $start,
2183
        $end=null,
2184
        $exclude=false,
2185
        $value=null,
2186
        $local=false
2187
    ) {
2188
        $types = (array) $types;
×
2189

2190
        if ($end === null) {
×
2191
            $end = 0;
×
2192
        }
2193

2194
        for ($i = $start; $i >= $end; $i--) {
×
2195
            $found = (bool) $exclude;
×
2196
            foreach ($types as $type) {
×
2197
                if ($this->tokens[$i]['code'] === $type) {
×
2198
                    $found = !$exclude;
×
2199
                    break;
×
2200
                }
2201
            }
2202

2203
            if ($found === true) {
×
2204
                if ($value === null) {
×
2205
                    return $i;
×
2206
                } else if ($this->tokens[$i]['content'] === $value) {
×
2207
                    return $i;
×
2208
                }
2209
            }
2210

2211
            if ($local === true) {
×
2212
                if (isset($this->tokens[$i]['scope_opener']) === true
×
2213
                    && $i === $this->tokens[$i]['scope_closer']
×
2214
                ) {
2215
                    $i = $this->tokens[$i]['scope_opener'];
×
2216
                } else if (isset($this->tokens[$i]['bracket_opener']) === true
×
2217
                    && $i === $this->tokens[$i]['bracket_closer']
×
2218
                ) {
2219
                    $i = $this->tokens[$i]['bracket_opener'];
×
2220
                } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
×
2221
                    && $i === $this->tokens[$i]['parenthesis_closer']
×
2222
                ) {
2223
                    $i = $this->tokens[$i]['parenthesis_opener'];
×
2224
                } else if ($this->tokens[$i]['code'] === T_SEMICOLON) {
×
2225
                    break;
×
2226
                }
2227
            }
2228
        }//end for
2229

2230
        return false;
×
2231

2232
    }//end findPrevious()
2233

2234

2235
    /**
2236
     * Returns the position of the next specified token(s).
2237
     *
2238
     * If a value is specified, the next token of the specified type(s)
2239
     * containing the specified value will be returned.
2240
     *
2241
     * Returns false if no token can be found.
2242
     *
2243
     * @param int|string|array $types   The type(s) of tokens to search for.
2244
     * @param int              $start   The position to start searching from in the
2245
     *                                  token stack.
2246
     * @param int|null         $end     The end position to fail if no token is found.
2247
     *                                  if not specified or null, end will default to
2248
     *                                  the end of the token stack.
2249
     * @param bool             $exclude If true, find the next token that is NOT of
2250
     *                                  a type specified in $types.
2251
     * @param string|null      $value   The value that the token(s) must be equal to.
2252
     *                                  If value is omitted, tokens with any value will
2253
     *                                  be returned.
2254
     * @param bool             $local   If true, tokens outside the current statement
2255
     *                                  will not be checked. i.e., checking will stop
2256
     *                                  at the next semicolon found.
2257
     *
2258
     * @return int|false
2259
     * @see    findPrevious()
2260
     */
2261
    public function findNext(
×
2262
        $types,
2263
        $start,
2264
        $end=null,
2265
        $exclude=false,
2266
        $value=null,
2267
        $local=false
2268
    ) {
2269
        $types = (array) $types;
×
2270

2271
        if ($end === null || $end > $this->numTokens) {
×
2272
            $end = $this->numTokens;
×
2273
        }
2274

2275
        for ($i = $start; $i < $end; $i++) {
×
2276
            $found = (bool) $exclude;
×
2277
            foreach ($types as $type) {
×
2278
                if ($this->tokens[$i]['code'] === $type) {
×
2279
                    $found = !$exclude;
×
2280
                    break;
×
2281
                }
2282
            }
2283

2284
            if ($found === true) {
×
2285
                if ($value === null) {
×
2286
                    return $i;
×
2287
                } else if ($this->tokens[$i]['content'] === $value) {
×
2288
                    return $i;
×
2289
                }
2290
            }
2291

2292
            if ($local === true && $this->tokens[$i]['code'] === T_SEMICOLON) {
×
2293
                break;
×
2294
            }
2295
        }//end for
2296

2297
        return false;
×
2298

2299
    }//end findNext()
2300

2301

2302
    /**
2303
     * Returns the position of the first non-whitespace token in a statement.
2304
     *
2305
     * @param int              $start  The position to start searching from in the token stack.
2306
     * @param int|string|array $ignore Token types that should not be considered stop points.
2307
     *
2308
     * @return int
2309
     */
2310
    public function findStartOfStatement($start, $ignore=null)
210✔
2311
    {
2312
        $startTokens = Tokens::$blockOpeners;
210✔
2313
        $startTokens[T_OPEN_SHORT_ARRAY]   = true;
210✔
2314
        $startTokens[T_OPEN_TAG]           = true;
210✔
2315
        $startTokens[T_OPEN_TAG_WITH_ECHO] = true;
210✔
2316

2317
        $endTokens = [
140✔
2318
            T_CLOSE_TAG    => true,
210✔
2319
            T_COLON        => true,
210✔
2320
            T_COMMA        => true,
210✔
2321
            T_DOUBLE_ARROW => true,
210✔
2322
            T_MATCH_ARROW  => true,
210✔
2323
            T_SEMICOLON    => true,
210✔
2324
        ];
140✔
2325

2326
        if ($ignore !== null) {
210✔
2327
            $ignore = (array) $ignore;
×
2328
            foreach ($ignore as $code) {
×
2329
                if (isset($startTokens[$code]) === true) {
×
2330
                    unset($startTokens[$code]);
×
2331
                }
2332

2333
                if (isset($endTokens[$code]) === true) {
×
2334
                    unset($endTokens[$code]);
×
2335
                }
2336
            }
2337
        }
2338

2339
        // If the start token is inside the case part of a match expression,
2340
        // find the start of the condition. If it's in the statement part, find
2341
        // the token that comes after the match arrow.
2342
        if (empty($this->tokens[$start]['conditions']) === false) {
210✔
2343
            $conditions         = $this->tokens[$start]['conditions'];
162✔
2344
            $lastConditionOwner = end($conditions);
162✔
2345
            $matchExpression    = key($conditions);
162✔
2346

2347
            if ($lastConditionOwner === T_MATCH
162✔
2348
                // Check if the $start token is at the same parentheses nesting level as the match token.
2349
                && ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === true
139✔
2350
                && empty($this->tokens[$start]['nested_parenthesis']) === true)
131✔
2351
                || ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === false
125✔
2352
                && empty($this->tokens[$start]['nested_parenthesis']) === false)
125✔
2353
                && $this->tokens[$matchExpression]['nested_parenthesis'] === $this->tokens[$start]['nested_parenthesis']))
162✔
2354
            ) {
2355
                // Walk back to the previous match arrow (if it exists).
2356
                $lastComma          = null;
45✔
2357
                $inNestedExpression = false;
45✔
2358
                for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
45✔
2359
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_MATCH_ARROW) {
45✔
2360
                        break;
33✔
2361
                    }
2362

2363
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_COMMA) {
45✔
2364
                        $lastComma = $prevMatch;
24✔
2365
                        continue;
24✔
2366
                    }
2367

2368
                    // Skip nested statements.
2369
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
45✔
2370
                        && $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
45✔
2371
                    ) {
2372
                        $prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
12✔
2373
                        continue;
12✔
2374
                    }
2375

2376
                    if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
45✔
2377
                        && $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
45✔
2378
                    ) {
2379
                        $prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
15✔
2380
                        continue;
15✔
2381
                    }
2382

2383
                    // Stop if we're _within_ a nested short array statement, which may contain comma's too.
2384
                    // No need to deal with parentheses, those are handled above via the `nested_parenthesis` checks.
2385
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
45✔
2386
                        && $this->tokens[$prevMatch]['bracket_closer'] > $start
45✔
2387
                    ) {
2388
                        $inNestedExpression = true;
15✔
2389
                        break;
15✔
2390
                    }
2391
                }//end for
2392

2393
                if ($inNestedExpression === false) {
45✔
2394
                    // $prevMatch will now either be the scope opener or a match arrow.
2395
                    // If it is the scope opener, go the first non-empty token after. $start will have been part of the first condition.
2396
                    if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) {
33✔
2397
                        // We're before the arrow in the first case.
2398
                        $next = $this->findNext(Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
12✔
2399
                        if ($next === false) {
12✔
2400
                            // Shouldn't be possible.
2401
                            return $start;
×
2402
                        }
2403

2404
                        return $next;
12✔
2405
                    }
2406

2407
                    // Okay, so we found a match arrow.
2408
                    // If $start was part of the "next" condition, the last comma will be set.
2409
                    // Otherwise, $start must have been part of a return expression.
2410
                    if (isset($lastComma) === true && $lastComma > $prevMatch) {
33✔
2411
                        $prevMatch = $lastComma;
15✔
2412
                    }
2413

2414
                    // In both cases, go to the first non-empty token after.
2415
                    $next = $this->findNext(Tokens::$emptyTokens, ($prevMatch + 1), null, true);
33✔
2416
                    if ($next === false) {
33✔
2417
                        // Shouldn't be possible.
2418
                        return $start;
×
2419
                    }
2420

2421
                    return $next;
33✔
2422
                }//end if
2423
            }//end if
2424
        }//end if
2425

2426
        $lastNotEmpty = $start;
180✔
2427

2428
        // If we are starting at a token that ends a scope block, skip to
2429
        // the start and continue from there.
2430
        // If we are starting at a token that ends a statement, skip this
2431
        // token so we find the true start of the statement.
2432
        while (isset($endTokens[$this->tokens[$start]['code']]) === true
180✔
2433
            || (isset($this->tokens[$start]['scope_condition']) === true
180✔
2434
            && $start === $this->tokens[$start]['scope_closer'])
180✔
2435
        ) {
2436
            if (isset($this->tokens[$start]['scope_condition']) === true) {
51✔
2437
                $start = $this->tokens[$start]['scope_condition'];
27✔
2438
            } else {
2439
                $start--;
30✔
2440
            }
2441
        }
2442

2443
        for ($i = $start; $i >= 0; $i--) {
180✔
2444
            if (isset($startTokens[$this->tokens[$i]['code']]) === true
180✔
2445
                || isset($endTokens[$this->tokens[$i]['code']]) === true
180✔
2446
            ) {
2447
                // Found the end of the previous statement.
2448
                return $lastNotEmpty;
180✔
2449
            }
2450

2451
            if (isset($this->tokens[$i]['scope_opener']) === true
177✔
2452
                && $i === $this->tokens[$i]['scope_closer']
177✔
2453
                && $this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS
177✔
2454
                && $this->tokens[$i]['code'] !== T_END_NOWDOC
177✔
2455
                && $this->tokens[$i]['code'] !== T_END_HEREDOC
177✔
2456
                && $this->tokens[$i]['code'] !== T_BREAK
177✔
2457
                && $this->tokens[$i]['code'] !== T_RETURN
177✔
2458
                && $this->tokens[$i]['code'] !== T_CONTINUE
177✔
2459
                && $this->tokens[$i]['code'] !== T_THROW
177✔
2460
                && $this->tokens[$i]['code'] !== T_EXIT
177✔
2461
                && $this->tokens[$i]['code'] !== T_GOTO
177✔
2462
            ) {
2463
                // Found the end of the previous scope block.
2464
                return $lastNotEmpty;
3✔
2465
            }
2466

2467
            // Skip nested statements.
2468
            if (isset($this->tokens[$i]['bracket_opener']) === true
177✔
2469
                && $i === $this->tokens[$i]['bracket_closer']
177✔
2470
            ) {
2471
                $i = $this->tokens[$i]['bracket_opener'];
3✔
2472
            } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
177✔
2473
                && $i === $this->tokens[$i]['parenthesis_closer']
177✔
2474
            ) {
2475
                $i = $this->tokens[$i]['parenthesis_opener'];
21✔
2476
            } else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
177✔
2477
                $start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
6✔
2478
                if ($start !== false) {
6✔
2479
                    $i = $start;
6✔
2480
                }
2481
            }//end if
2482

2483
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
177✔
2484
                $lastNotEmpty = $i;
177✔
2485
            }
2486
        }//end for
2487

2488
        return 0;
×
2489

2490
    }//end findStartOfStatement()
2491

2492

2493
    /**
2494
     * Returns the position of the last non-whitespace token in a statement.
2495
     *
2496
     * @param int              $start  The position to start searching from in the token stack.
2497
     * @param int|string|array $ignore Token types that should not be considered stop points.
2498
     *
2499
     * @return int
2500
     */
2501
    public function findEndOfStatement($start, $ignore=null)
66✔
2502
    {
2503
        $endTokens = [
44✔
2504
            T_COLON                => true,
66✔
2505
            T_COMMA                => true,
66✔
2506
            T_DOUBLE_ARROW         => true,
66✔
2507
            T_SEMICOLON            => true,
66✔
2508
            T_CLOSE_PARENTHESIS    => true,
66✔
2509
            T_CLOSE_SQUARE_BRACKET => true,
66✔
2510
            T_CLOSE_CURLY_BRACKET  => true,
66✔
2511
            T_CLOSE_SHORT_ARRAY    => true,
66✔
2512
            T_OPEN_TAG             => true,
66✔
2513
            T_CLOSE_TAG            => true,
66✔
2514
        ];
44✔
2515

2516
        if ($ignore !== null) {
66✔
2517
            $ignore = (array) $ignore;
×
2518
            foreach ($ignore as $code) {
×
2519
                unset($endTokens[$code]);
×
2520
            }
2521
        }
2522

2523
        // If the start token is inside the case part of a match expression,
2524
        // advance to the match arrow and continue looking for the
2525
        // end of the statement from there so that we skip over commas.
2526
        if ($this->tokens[$start]['code'] !== T_MATCH_ARROW) {
66✔
2527
            $matchExpression = $this->getCondition($start, T_MATCH);
66✔
2528
            if ($matchExpression !== false) {
66✔
2529
                $beforeArrow    = true;
30✔
2530
                $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']);
30✔
2531
                if ($prevMatchArrow !== false) {
30✔
2532
                    $prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start);
27✔
2533
                    if ($prevComma === false) {
27✔
2534
                        // No comma between this token and the last match arrow,
2535
                        // so this token exists after the arrow and we can continue
2536
                        // checking as normal.
2537
                        $beforeArrow = false;
12✔
2538
                    }
2539
                }
2540

2541
                if ($beforeArrow === true) {
30✔
2542
                    $nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
30✔
2543
                    if ($nextMatchArrow !== false) {
30✔
2544
                        $start = $nextMatchArrow;
30✔
2545
                    }
2546
                }
2547
            }//end if
2548
        }//end if
2549

2550
        $lastNotEmpty = $start;
66✔
2551
        for ($i = $start; $i < $this->numTokens; $i++) {
66✔
2552
            if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
66✔
2553
                // Found the end of the statement.
2554
                if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS
60✔
2555
                    || $this->tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
57✔
2556
                    || $this->tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
57✔
2557
                    || $this->tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
51✔
2558
                    || $this->tokens[$i]['code'] === T_OPEN_TAG
48✔
2559
                    || $this->tokens[$i]['code'] === T_CLOSE_TAG
60✔
2560
                ) {
2561
                    return $lastNotEmpty;
24✔
2562
                }
2563

2564
                return $i;
48✔
2565
            }
2566

2567
            // Skip nested statements.
2568
            if (isset($this->tokens[$i]['scope_closer']) === true
66✔
2569
                && ($i === $this->tokens[$i]['scope_opener']
56✔
2570
                || $i === $this->tokens[$i]['scope_condition'])
66✔
2571
            ) {
2572
                if ($this->tokens[$i]['code'] === T_FN) {
36✔
2573
                    $lastNotEmpty = $this->tokens[$i]['scope_closer'];
18✔
2574
                    $i            = ($this->tokens[$i]['scope_closer'] - 1);
18✔
2575
                    continue;
18✔
2576
                }
2577

2578
                if ($i === $start && isset(Tokens::$scopeOpeners[$this->tokens[$i]['code']]) === true) {
21✔
2579
                    return $this->tokens[$i]['scope_closer'];
9✔
2580
                }
2581

2582
                $i = $this->tokens[$i]['scope_closer'];
15✔
2583
            } else if (isset($this->tokens[$i]['bracket_closer']) === true
48✔
2584
                && $i === $this->tokens[$i]['bracket_opener']
48✔
2585
            ) {
2586
                $i = $this->tokens[$i]['bracket_closer'];
6✔
2587
            } else if (isset($this->tokens[$i]['parenthesis_closer']) === true
48✔
2588
                && $i === $this->tokens[$i]['parenthesis_opener']
48✔
2589
            ) {
2590
                $i = $this->tokens[$i]['parenthesis_closer'];
9✔
2591
            } else if ($this->tokens[$i]['code'] === T_OPEN_USE_GROUP) {
48✔
2592
                $end = $this->findNext(T_CLOSE_USE_GROUP, ($i + 1));
6✔
2593
                if ($end !== false) {
6✔
2594
                    $i = $end;
6✔
2595
                }
2596
            }//end if
2597

2598
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
48✔
2599
                $lastNotEmpty = $i;
48✔
2600
            }
2601
        }//end for
2602

2603
        return ($this->numTokens - 1);
3✔
2604

2605
    }//end findEndOfStatement()
2606

2607

2608
    /**
2609
     * Returns the position of the first token on a line, matching given type.
2610
     *
2611
     * Returns false if no token can be found.
2612
     *
2613
     * @param int|string|array $types   The type(s) of tokens to search for.
2614
     * @param int              $start   The position to start searching from in the
2615
     *                                  token stack.
2616
     * @param bool             $exclude If true, find the token that is NOT of
2617
     *                                  the types specified in $types.
2618
     * @param string           $value   The value that the token must be equal to.
2619
     *                                  If value is omitted, tokens with any value will
2620
     *                                  be returned.
2621
     *
2622
     * @return int|false The first token which matches on the line containing the start
2623
     *                   token, between the start of the line and the start token.
2624
     *                   Note: The first token matching might be the start token.
2625
     *                   FALSE when no matching token could be found between the start of
2626
     *                   the line and the start token.
2627
     */
2628
    public function findFirstOnLine($types, $start, $exclude=false, $value=null)
×
2629
    {
2630
        if (is_array($types) === false) {
×
2631
            $types = [$types];
×
2632
        }
2633

2634
        $foundToken = false;
×
2635

2636
        for ($i = $start; $i >= 0; $i--) {
×
2637
            if ($this->tokens[$i]['line'] < $this->tokens[$start]['line']) {
×
2638
                break;
×
2639
            }
2640

2641
            $found = $exclude;
×
2642
            foreach ($types as $type) {
×
2643
                if ($exclude === false) {
×
2644
                    if ($this->tokens[$i]['code'] === $type) {
×
2645
                        $found = true;
×
2646
                        break;
×
2647
                    }
2648
                } else {
2649
                    if ($this->tokens[$i]['code'] === $type) {
×
2650
                        $found = false;
×
2651
                        break;
×
2652
                    }
2653
                }
2654
            }
2655

2656
            if ($found === true) {
×
2657
                if ($value === null) {
×
2658
                    $foundToken = $i;
×
2659
                } else if ($this->tokens[$i]['content'] === $value) {
×
2660
                    $foundToken = $i;
×
2661
                }
2662
            }
2663
        }//end for
2664

2665
        return $foundToken;
×
2666

2667
    }//end findFirstOnLine()
2668

2669

2670
    /**
2671
     * Determine if the passed token has a condition of one of the passed types.
2672
     *
2673
     * @param int              $stackPtr The position of the token we are checking.
2674
     * @param int|string|array $types    The type(s) of tokens to search for.
2675
     *
2676
     * @return boolean
2677
     */
2678
    public function hasCondition($stackPtr, $types)
21✔
2679
    {
2680
        // Check for the existence of the token.
2681
        if (isset($this->tokens[$stackPtr]) === false) {
21✔
2682
            return false;
3✔
2683
        }
2684

2685
        // Make sure the token has conditions.
2686
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
18✔
UNCOV
2687
            return false;
×
2688
        }
2689

2690
        $types      = (array) $types;
18✔
2691
        $conditions = $this->tokens[$stackPtr]['conditions'];
18✔
2692

2693
        foreach ($types as $type) {
18✔
2694
            if (in_array($type, $conditions, true) === true) {
18✔
2695
                // We found a token with the required type.
2696
                return true;
15✔
2697
            }
2698
        }
2699

2700
        return false;
18✔
2701

2702
    }//end hasCondition()
2703

2704

2705
    /**
2706
     * Return the position of the condition for the passed token.
2707
     *
2708
     * Returns FALSE if the token does not have the condition.
2709
     *
2710
     * @param int        $stackPtr The position of the token we are checking.
2711
     * @param int|string $type     The type of token to search for.
2712
     * @param bool       $first    If TRUE, will return the matched condition
2713
     *                             furthest away from the passed token.
2714
     *                             If FALSE, will return the matched condition
2715
     *                             closest to the passed token.
2716
     *
2717
     * @return int|false
2718
     */
2719
    public function getCondition($stackPtr, $type, $first=true)
30✔
2720
    {
2721
        // Check for the existence of the token.
2722
        if (isset($this->tokens[$stackPtr]) === false) {
30✔
2723
            return false;
3✔
2724
        }
2725

2726
        // Make sure the token has conditions.
2727
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
27✔
UNCOV
2728
            return false;
×
2729
        }
2730

2731
        $conditions = $this->tokens[$stackPtr]['conditions'];
27✔
2732
        if ($first === false) {
27✔
2733
            $conditions = array_reverse($conditions, true);
12✔
2734
        }
2735

2736
        foreach ($conditions as $token => $condition) {
27✔
2737
            if ($condition === $type) {
27✔
2738
                return $token;
24✔
2739
            }
2740
        }
2741

2742
        return false;
27✔
2743

2744
    }//end getCondition()
2745

2746

2747
    /**
2748
     * Returns the name of the class that the specified class extends.
2749
     * (works for classes, anonymous classes and interfaces)
2750
     *
2751
     * Returns FALSE on error or if there is no extended class name.
2752
     *
2753
     * @param int $stackPtr The stack position of the class.
2754
     *
2755
     * @return string|false
2756
     */
2757
    public function findExtendedClassName($stackPtr)
54✔
2758
    {
2759
        // Check for the existence of the token.
2760
        if (isset($this->tokens[$stackPtr]) === false) {
54✔
2761
            return false;
3✔
2762
        }
2763

2764
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
51✔
2765
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
51✔
2766
            && $this->tokens[$stackPtr]['code'] !== T_INTERFACE
51✔
2767
        ) {
2768
            return false;
3✔
2769
        }
2770

2771
        if (isset($this->tokens[$stackPtr]['scope_opener']) === false) {
48✔
2772
            return false;
3✔
2773
        }
2774

2775
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
45✔
2776
        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex);
45✔
2777
        if ($extendsIndex === false) {
45✔
2778
            return false;
9✔
2779
        }
2780

2781
        $find   = Tokens::$nameTokens;
36✔
2782
        $find[] = T_WHITESPACE;
36✔
2783

2784
        $end  = $this->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true);
36✔
2785
        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
36✔
2786
        $name = trim($name);
36✔
2787

2788
        if ($name === '') {
36✔
2789
            return false;
3✔
2790
        }
2791

2792
        return $name;
33✔
2793

2794
    }//end findExtendedClassName()
2795

2796

2797
    /**
2798
     * Returns the names of the interfaces that the specified class or enum implements.
2799
     *
2800
     * Returns FALSE on error or if there are no implemented interface names.
2801
     *
2802
     * @param int $stackPtr The stack position of the class or enum token.
2803
     *
2804
     * @return array|false
2805
     */
2806
    public function findImplementedInterfaceNames($stackPtr)
51✔
2807
    {
2808
        // Check for the existence of the token.
2809
        if (isset($this->tokens[$stackPtr]) === false) {
51✔
2810
            return false;
3✔
2811
        }
2812

2813
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
48✔
2814
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
48✔
2815
            && $this->tokens[$stackPtr]['code'] !== T_ENUM
48✔
2816
        ) {
2817
            return false;
6✔
2818
        }
2819

2820
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
42✔
2821
            return false;
3✔
2822
        }
2823

2824
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
39✔
2825
        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
39✔
2826
        if ($implementsIndex === false) {
39✔
2827
            return false;
6✔
2828
        }
2829

2830
        $find   = Tokens::$nameTokens;
33✔
2831
        $find[] = T_WHITESPACE;
33✔
2832
        $find[] = T_COMMA;
33✔
2833

2834
        $end  = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
33✔
2835
        $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
33✔
2836
        $name = trim($name);
33✔
2837

2838
        if ($name === '') {
33✔
2839
            return false;
3✔
2840
        } else {
2841
            $names = explode(',', $name);
30✔
2842
            $names = array_map('trim', $names);
30✔
2843
            return $names;
30✔
2844
        }
2845

2846
    }//end findImplementedInterfaceNames()
2847

2848

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