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

PHPCSStandards / PHP_CodeSniffer / 14454424553

14 Apr 2025 07:49PM UTC coverage: 77.579% (+2.3%) from 75.291%
14454424553

push

github

web-flow
Merge pull request #983 from PHPCSStandards/phpcs-4.0/feature/sq-2448-remove-support-js-css

Remove CSS/JS support (HUGE PR, reviews welcome!)

119 of 126 new or added lines in 14 files covered. (94.44%)

26 existing lines in 10 files now uncovered.

19384 of 24986 relevant lines covered (77.58%)

78.47 hits per line

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

70.5
/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

21
class File
22
{
23

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

154
    /**
155
     * The total number of errors and warnings that can be fixed.
156
     *
157
     * @var integer
158
     */
159
    protected $fixableCount = 0;
160

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

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

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

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

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

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

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

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

220

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

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

247
    }//end __construct()
248

249

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

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

271
    }//end setContent()
272

273

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

285
    }//end reloadContent()
×
286

287

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

297
    }//end disableCaching()
298

299

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

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

317
        $this->parse();
×
318

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

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

326
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
327
            echo "\t*** START TOKEN PROCESSING ***".PHP_EOL;
×
328
        }
329

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

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

377
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
378
                $type    = $token['type'];
×
379
                $content = Common::prepareForOutput($token['content']);
×
380
                echo "\t\tProcess token $stackPtr: $type => $content".PHP_EOL;
×
381
            }
382

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

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

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

UNCOV
400
                $class = $listenerData['class'];
×
401

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

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

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

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

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

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

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

454
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
455
                    echo "\t\t\tProcessing ".$this->activeListener.'... ';
×
456
                }
457

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

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

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

472
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
473
                    $timeTaken = round(($timeTaken), 4);
×
474
                    echo "DONE in $timeTaken seconds".PHP_EOL;
×
475
                }
476

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

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

495
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
496
            echo "\t*** END TOKEN PROCESSING ***".PHP_EOL;
×
497
            echo "\t*** START SNIFF PROCESSING REPORT ***".PHP_EOL;
×
498

499
            arsort($this->listenerTimes, SORT_NUMERIC);
×
500
            foreach ($this->listenerTimes as $listener => $timeTaken) {
×
501
                echo "\t$listener: ".round(($timeTaken), 4).' secs'.PHP_EOL;
×
502
            }
503

504
            echo "\t*** END SNIFF PROCESSING REPORT ***".PHP_EOL;
×
505
        }
506

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

509
    }//end process()
510

511

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

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

537
            return;
×
538
        }
539

540
        $this->numTokens = count($this->tokens);
×
541

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

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

NEW
563
            echo "[$this->numTokens tokens in $numLines lines]... ";
×
564
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
565
                echo PHP_EOL;
×
566
            }
567
        }
568

569
    }//end parse()
570

571

572
    /**
573
     * Returns the token stack for this file.
574
     *
575
     * @return array
576
     */
577
    public function getTokens()
×
578
    {
579
        return $this->tokens;
×
580

581
    }//end getTokens()
582

583

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

600
    }//end cleanUp()
601

602

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

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

634
    }//end addError()
635

636

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

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

668
    }//end addWarning()
669

670

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

692
    }//end addErrorOnLine()
693

694

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

716
    }//end addWarningOnLine()
717

718

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

745
        return false;
×
746

747
    }//end addFixableError()
748

749

750
    /**
751
     * Records a fixable warning against a specific token in the file.
752
     *
753
     * Returns true if the warning was recorded and should be fixed.
754
     *
755
     * @param string $warning  The error message.
756
     * @param int    $stackPtr The stack position where the error occurred.
757
     * @param string $code     A violation code unique to the sniff message.
758
     * @param array  $data     Replacements for the warning message.
759
     * @param int    $severity The severity level for this warning. A value of 0
760
     *                         will be converted into the default severity level.
761
     *
762
     * @return boolean
763
     */
764
    public function addFixableWarning(
×
765
        $warning,
766
        $stackPtr,
767
        $code,
768
        $data=[],
769
        $severity=0
770
    ) {
771
        $recorded = $this->addWarning($warning, $stackPtr, $code, $data, $severity, true);
×
772
        if ($recorded === true && $this->fixer->enabled === true) {
×
773
            return true;
×
774
        }
775

776
        return false;
×
777

778
    }//end addFixableWarning()
779

780

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

803
        // Work out which sniff generated the message.
804
        $parts = explode('.', $code);
168✔
805
        if ($parts[0] === 'Internal') {
168✔
806
            // An internal message.
807
            $listenerCode = '';
×
808
            if ($this->activeListener !== '') {
×
809
                $listenerCode = Common::getSniffCode($this->activeListener);
×
810
            }
811

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

825
            $checkCodes = [
112✔
826
                $sniffCode,
168✔
827
                $parts[0].'.'.$parts[1].'.'.$parts[2],
168✔
828
                $parts[0].'.'.$parts[1],
168✔
829
                $parts[0],
168✔
830
            ];
112✔
831
        }//end if
832

833
        if (isset($this->tokenizer->ignoredLines[$line]) === true) {
168✔
834
            // Check if this line is ignoring this specific message.
835
            $ignored = false;
99✔
836
            foreach ($checkCodes as $checkCode) {
99✔
837
                if (isset($this->tokenizer->ignoredLines[$line][$checkCode]) === true) {
99✔
838
                    $ignored = true;
90✔
839
                    break;
90✔
840
                }
841
            }
842

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

855
            if ($ignored === true) {
99✔
856
                return false;
90✔
857
            }
858
        }//end if
859

860
        $includeAll = true;
156✔
861
        if ($this->configCache['cache'] === false
156✔
862
            || $this->configCache['recordErrors'] === false
156✔
863
        ) {
864
            $includeAll = false;
156✔
865
        }
866

867
        // Filter out any messages for sniffs that shouldn't have run
868
        // due to the use of the --sniffs command line argument.
869
        if ($includeAll === false
156✔
870
            && ((empty($this->configCache['sniffs']) === false
156✔
871
            && in_array(strtolower($listenerCode), $this->configCache['sniffs'], true) === false)
156✔
872
            || (empty($this->configCache['exclude']) === false
156✔
873
            && in_array(strtolower($listenerCode), $this->configCache['exclude'], true) === true))
156✔
874
        ) {
875
            return false;
×
876
        }
877

878
        // If we know this sniff code is being ignored for this file, return early.
879
        foreach ($checkCodes as $checkCode) {
156✔
880
            if (isset($this->ignoredCodes[$checkCode]) === true) {
156✔
881
                return false;
×
882
            }
883
        }
884

885
        $oppositeType = 'warning';
156✔
886
        if ($error === false) {
156✔
887
            $oppositeType = 'error';
75✔
888
        }
889

890
        foreach ($checkCodes as $checkCode) {
156✔
891
            // Make sure this message type has not been set to the opposite message type.
892
            if (isset($this->ruleset->ruleset[$checkCode]['type']) === true
156✔
893
                && $this->ruleset->ruleset[$checkCode]['type'] === $oppositeType
156✔
894
            ) {
895
                $error = !$error;
×
896
                break;
×
897
            }
898
        }
899

900
        if ($error === true) {
156✔
901
            $configSeverity = $this->configCache['errorSeverity'];
132✔
902
            $messageCount   = &$this->errorCount;
132✔
903
            $messages       = &$this->errors;
132✔
904
        } else {
905
            $configSeverity = $this->configCache['warningSeverity'];
75✔
906
            $messageCount   = &$this->warningCount;
75✔
907
            $messages       = &$this->warnings;
75✔
908
        }
909

910
        if ($includeAll === false && $configSeverity === 0) {
156✔
911
            // Don't bother doing any processing as these messages are just going to
912
            // be hidden in the reports anyway.
913
            return false;
×
914
        }
915

916
        if ($severity === 0) {
156✔
917
            $severity = 5;
156✔
918
        }
919

920
        foreach ($checkCodes as $checkCode) {
156✔
921
            // Make sure we are interested in this severity level.
922
            if (isset($this->ruleset->ruleset[$checkCode]['severity']) === true) {
156✔
923
                $severity = $this->ruleset->ruleset[$checkCode]['severity'];
×
924
                break;
×
925
            }
926
        }
927

928
        if ($includeAll === false && $configSeverity > $severity) {
156✔
929
            return false;
×
930
        }
931

932
        // Make sure we are not ignoring this file.
933
        $included = null;
156✔
934
        if (trim($this->path, '\'"') === 'STDIN') {
156✔
935
            $included = true;
156✔
936
        } else {
937
            foreach ($checkCodes as $checkCode) {
×
938
                $patterns = null;
×
939

940
                if (isset($this->configCache['includePatterns'][$checkCode]) === true) {
×
941
                    $patterns  = $this->configCache['includePatterns'][$checkCode];
×
942
                    $excluding = false;
×
943
                } else if (isset($this->configCache['ignorePatterns'][$checkCode]) === true) {
×
944
                    $patterns  = $this->configCache['ignorePatterns'][$checkCode];
×
945
                    $excluding = true;
×
946
                }
947

948
                if ($patterns === null) {
×
949
                    continue;
×
950
                }
951

952
                foreach ($patterns as $pattern => $type) {
×
953
                    // While there is support for a type of each pattern
954
                    // (absolute or relative) we don't actually support it here.
955
                    $replacements = [
956
                        '\\,' => ',',
×
957
                        '*'   => '.*',
958
                    ];
959

960
                    // We assume a / directory separator, as do the exclude rules
961
                    // most developers write, so we need a special case for any system
962
                    // that is different.
963
                    if (DIRECTORY_SEPARATOR === '\\') {
×
964
                        $replacements['/'] = '\\\\';
×
965
                    }
966

967
                    $pattern = '`'.strtr($pattern, $replacements).'`i';
×
968
                    $matched = preg_match($pattern, $this->path);
×
969

970
                    if ($matched === 0) {
×
971
                        if ($excluding === false && $included === null) {
×
972
                            // This file path is not being included.
973
                            $included = false;
×
974
                        }
975

976
                        continue;
×
977
                    }
978

979
                    if ($excluding === true) {
×
980
                        // This file path is being excluded.
981
                        $this->ignoredCodes[$checkCode] = true;
×
982
                        return false;
×
983
                    }
984

985
                    // This file path is being included.
986
                    $included = true;
×
987
                    break;
×
988
                }//end foreach
989
            }//end foreach
990
        }//end if
991

992
        if ($included === false) {
156✔
993
            // There were include rules set, but this file
994
            // path didn't match any of them.
995
            return false;
×
996
        }
997

998
        $messageCount++;
156✔
999
        if ($fixable === true) {
156✔
1000
            $this->fixableCount++;
132✔
1001
        }
1002

1003
        if ($this->configCache['recordErrors'] === false
156✔
1004
            && $includeAll === false
156✔
1005
        ) {
1006
            return true;
×
1007
        }
1008

1009
        // See if there is a custom error message format to use.
1010
        // But don't do this if we are replaying errors because replayed
1011
        // errors have already used the custom format and have had their
1012
        // data replaced.
1013
        if ($this->replayingErrors === false
156✔
1014
            && isset($this->ruleset->ruleset[$sniffCode]['message']) === true
156✔
1015
        ) {
1016
            $message = $this->ruleset->ruleset[$sniffCode]['message'];
×
1017
        }
1018

1019
        if (empty($data) === false) {
156✔
1020
            $message = vsprintf($message, $data);
153✔
1021
        }
1022

1023
        if (isset($messages[$line]) === false) {
156✔
1024
            $messages[$line] = [];
156✔
1025
        }
1026

1027
        if (isset($messages[$line][$column]) === false) {
156✔
1028
            $messages[$line][$column] = [];
156✔
1029
        }
1030

1031
        $messages[$line][$column][] = [
156✔
1032
            'message'  => $message,
156✔
1033
            'source'   => $sniffCode,
156✔
1034
            'listener' => $this->activeListener,
156✔
1035
            'severity' => $severity,
156✔
1036
            'fixable'  => $fixable,
156✔
1037
        ];
104✔
1038

1039
        if (PHP_CODESNIFFER_VERBOSITY > 1
156✔
1040
            && $this->fixer->enabled === true
156✔
1041
            && $fixable === true
156✔
1042
        ) {
1043
            @ob_end_clean();
×
1044
            echo "\tE: [Line $line] $message ($sniffCode)".PHP_EOL;
×
1045
            ob_start();
×
1046
        }
1047

1048
        return true;
156✔
1049

1050
    }//end addMessage()
1051

1052

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

1076
        return true;
×
1077

1078
    }//end recordMetric()
1079

1080

1081
    /**
1082
     * Returns the number of errors raised.
1083
     *
1084
     * @return int
1085
     */
1086
    public function getErrorCount()
×
1087
    {
1088
        return $this->errorCount;
×
1089

1090
    }//end getErrorCount()
1091

1092

1093
    /**
1094
     * Returns the number of warnings raised.
1095
     *
1096
     * @return int
1097
     */
1098
    public function getWarningCount()
×
1099
    {
1100
        return $this->warningCount;
×
1101

1102
    }//end getWarningCount()
1103

1104

1105
    /**
1106
     * Returns the number of fixable errors/warnings raised.
1107
     *
1108
     * @return int
1109
     */
1110
    public function getFixableCount()
×
1111
    {
1112
        return $this->fixableCount;
×
1113

1114
    }//end getFixableCount()
1115

1116

1117
    /**
1118
     * Returns the number of fixed errors/warnings.
1119
     *
1120
     * @return int
1121
     */
1122
    public function getFixedCount()
×
1123
    {
1124
        return $this->fixedCount;
×
1125

1126
    }//end getFixedCount()
1127

1128

1129
    /**
1130
     * Returns the list of ignored lines.
1131
     *
1132
     * @return array
1133
     */
1134
    public function getIgnoredLines()
×
1135
    {
1136
        return $this->tokenizer->ignoredLines;
×
1137

1138
    }//end getIgnoredLines()
1139

1140

1141
    /**
1142
     * Returns the errors raised from processing this file.
1143
     *
1144
     * @return array
1145
     */
1146
    public function getErrors()
×
1147
    {
1148
        return $this->errors;
×
1149

1150
    }//end getErrors()
1151

1152

1153
    /**
1154
     * Returns the warnings raised from processing this file.
1155
     *
1156
     * @return array
1157
     */
1158
    public function getWarnings()
×
1159
    {
1160
        return $this->warnings;
×
1161

1162
    }//end getWarnings()
1163

1164

1165
    /**
1166
     * Returns the metrics found while processing this file.
1167
     *
1168
     * @return array
1169
     */
1170
    public function getMetrics()
×
1171
    {
1172
        return $this->metrics;
×
1173

1174
    }//end getMetrics()
1175

1176

1177
    /**
1178
     * Returns the time taken processing this file for each invoked sniff.
1179
     *
1180
     * @return array
1181
     */
1182
    public function getListenerTimes()
×
1183
    {
1184
        return $this->listenerTimes;
×
1185

1186
    }//end getListenerTimes()
1187

1188

1189
    /**
1190
     * Returns the absolute filename of this file.
1191
     *
1192
     * @return string
1193
     */
1194
    public function getFilename()
×
1195
    {
1196
        return $this->path;
×
1197

1198
    }//end getFilename()
1199

1200

1201
    /**
1202
     * Returns the declaration name for classes, interfaces, traits, enums, and functions.
1203
     *
1204
     * @param int $stackPtr The position of the declaration token which
1205
     *                      declared the class, interface, trait, or function.
1206
     *
1207
     * @return string|null The name of the class, interface, trait, or function;
1208
     *                     or NULL if the function or class is anonymous.
1209
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type
1210
     *                                                      T_FUNCTION, T_CLASS, T_ANON_CLASS,
1211
     *                                                      T_CLOSURE, T_TRAIT, T_ENUM, or T_INTERFACE.
1212
     */
1213
    public function getDeclarationName($stackPtr)
87✔
1214
    {
1215
        $tokenCode = $this->tokens[$stackPtr]['code'];
87✔
1216

1217
        if ($tokenCode === T_ANON_CLASS || $tokenCode === T_CLOSURE) {
87✔
1218
            return null;
15✔
1219
        }
1220

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

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

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

1247
        return $content;
69✔
1248

1249
    }//end getDeclarationName()
1250

1251

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1565
        return $vars;
207✔
1566

1567
    }//end getMethodParameters()
1568

1569

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1755
    }//end getMethodProperties()
1756

1757

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

1794
        $conditions = array_keys($this->tokens[$stackPtr]['conditions']);
297✔
1795
        $ptr        = array_pop($conditions);
297✔
1796
        if (isset($this->tokens[$ptr]) === false
297✔
1797
            || ($this->tokens[$ptr]['code'] !== T_CLASS
296✔
1798
            && $this->tokens[$ptr]['code'] !== T_ANON_CLASS
296✔
1799
            && $this->tokens[$ptr]['code'] !== T_TRAIT)
297✔
1800
        ) {
1801
            if (isset($this->tokens[$ptr]) === true
18✔
1802
                && ($this->tokens[$ptr]['code'] === T_INTERFACE
17✔
1803
                || $this->tokens[$ptr]['code'] === T_ENUM)
18✔
1804
            ) {
1805
                // T_VARIABLEs in interfaces/enums can actually be method arguments
1806
                // but they won't be seen as being inside the method because there
1807
                // are no scope openers and closers for abstract methods. If it is in
1808
                // parentheses, we can be pretty sure it is a method argument.
1809
                if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === false
9✔
1810
                    || empty($this->tokens[$stackPtr]['nested_parenthesis']) === true
9✔
1811
                ) {
1812
                    $error = 'Possible parse error: %ss may not include member vars';
6✔
1813
                    $code  = sprintf('Internal.ParseError.%sHasMemberVar', ucfirst($this->tokens[$ptr]['content']));
6✔
1814
                    $data  = [strtolower($this->tokens[$ptr]['content'])];
6✔
1815
                    $this->addWarning($error, $stackPtr, $code, $data);
6✔
1816
                    return [];
8✔
1817
                }
1818
            } else {
1819
                throw new RuntimeException('$stackPtr is not a class member var');
9✔
1820
            }
1821
        }//end if
1822

1823
        // Make sure it's not a method parameter.
1824
        if (empty($this->tokens[$stackPtr]['nested_parenthesis']) === false) {
282✔
1825
            $parenthesis = array_keys($this->tokens[$stackPtr]['nested_parenthesis']);
18✔
1826
            $deepestOpen = array_pop($parenthesis);
18✔
1827
            if ($deepestOpen > $ptr
18✔
1828
                && isset($this->tokens[$deepestOpen]['parenthesis_owner']) === true
18✔
1829
                && $this->tokens[$this->tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION
18✔
1830
            ) {
1831
                throw new RuntimeException('$stackPtr is not a class member var');
12✔
1832
            }
1833
        }
1834

1835
        $valid = [
180✔
1836
            T_PUBLIC    => T_PUBLIC,
270✔
1837
            T_PRIVATE   => T_PRIVATE,
270✔
1838
            T_PROTECTED => T_PROTECTED,
270✔
1839
            T_STATIC    => T_STATIC,
270✔
1840
            T_VAR       => T_VAR,
270✔
1841
            T_READONLY  => T_READONLY,
270✔
1842
            T_FINAL     => T_FINAL,
270✔
1843
        ];
180✔
1844

1845
        $valid += Tokens::$emptyTokens;
270✔
1846

1847
        $scope          = 'public';
270✔
1848
        $scopeSpecified = false;
270✔
1849
        $isStatic       = false;
270✔
1850
        $isReadonly     = false;
270✔
1851
        $isFinal        = false;
270✔
1852

1853
        $startOfStatement = $this->findPrevious(
270✔
1854
            [
180✔
1855
                T_SEMICOLON,
270✔
1856
                T_OPEN_CURLY_BRACKET,
270✔
1857
                T_CLOSE_CURLY_BRACKET,
270✔
1858
                T_ATTRIBUTE_END,
270✔
1859
            ],
180✔
1860
            ($stackPtr - 1)
270✔
1861
        );
180✔
1862

1863
        for ($i = ($startOfStatement + 1); $i < $stackPtr; $i++) {
270✔
1864
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
270✔
1865
                break;
207✔
1866
            }
1867

1868
            switch ($this->tokens[$i]['code']) {
270✔
1869
            case T_PUBLIC:
270✔
1870
                $scope          = 'public';
129✔
1871
                $scopeSpecified = true;
129✔
1872
                break;
129✔
1873
            case T_PRIVATE:
270✔
1874
                $scope          = 'private';
57✔
1875
                $scopeSpecified = true;
57✔
1876
                break;
57✔
1877
            case T_PROTECTED:
270✔
1878
                $scope          = 'protected';
42✔
1879
                $scopeSpecified = true;
42✔
1880
                break;
42✔
1881
            case T_STATIC:
270✔
1882
                $isStatic = true;
66✔
1883
                break;
66✔
1884
            case T_READONLY:
270✔
1885
                $isReadonly = true;
36✔
1886
                break;
36✔
1887
            case T_FINAL:
270✔
1888
                $isFinal = true;
27✔
1889
                break;
27✔
1890
            }//end switch
1891
        }//end for
1892

1893
        $type         = '';
270✔
1894
        $typeToken    = false;
270✔
1895
        $typeEndToken = false;
270✔
1896
        $nullableType = false;
270✔
1897

1898
        if ($i < $stackPtr) {
270✔
1899
            // We've found a type.
1900
            $valid = [
138✔
1901
                T_STRING                 => T_STRING,
207✔
1902
                T_CALLABLE               => T_CALLABLE,
207✔
1903
                T_SELF                   => T_SELF,
207✔
1904
                T_PARENT                 => T_PARENT,
207✔
1905
                T_FALSE                  => T_FALSE,
207✔
1906
                T_TRUE                   => T_TRUE,
207✔
1907
                T_NULL                   => T_NULL,
207✔
1908
                T_NAMESPACE              => T_NAMESPACE,
207✔
1909
                T_NS_SEPARATOR           => T_NS_SEPARATOR,
207✔
1910
                T_TYPE_UNION             => T_TYPE_UNION,
207✔
1911
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
207✔
1912
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
207✔
1913
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
207✔
1914
            ];
138✔
1915

1916
            for ($i; $i < $stackPtr; $i++) {
207✔
1917
                if ($this->tokens[$i]['code'] === T_VARIABLE) {
207✔
1918
                    // Hit another variable in a group definition.
1919
                    break;
30✔
1920
                }
1921

1922
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
183✔
1923
                    $nullableType = true;
51✔
1924
                }
1925

1926
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
183✔
1927
                    $typeEndToken = $i;
183✔
1928
                    if ($typeToken === false) {
183✔
1929
                        $typeToken = $i;
183✔
1930
                    }
1931

1932
                    $type .= $this->tokens[$i]['content'];
183✔
1933
                }
1934
            }
1935

1936
            if ($type !== '' && $nullableType === true) {
207✔
1937
                $type = '?'.$type;
51✔
1938
            }
1939
        }//end if
1940

1941
        return [
180✔
1942
            'scope'           => $scope,
270✔
1943
            'scope_specified' => $scopeSpecified,
270✔
1944
            'is_static'       => $isStatic,
270✔
1945
            'is_readonly'     => $isReadonly,
270✔
1946
            'is_final'        => $isFinal,
270✔
1947
            'type'            => $type,
270✔
1948
            'type_token'      => $typeToken,
270✔
1949
            'type_end_token'  => $typeEndToken,
270✔
1950
            'nullable_type'   => $nullableType,
270✔
1951
        ];
180✔
1952

1953
    }//end getMemberProperties()
1954

1955

1956
    /**
1957
     * Returns the visibility and implementation properties of a class.
1958
     *
1959
     * The format of the return value is:
1960
     * <code>
1961
     *   array(
1962
     *    'is_abstract' => boolean, // TRUE if the abstract keyword was found.
1963
     *    'is_final'    => boolean, // TRUE if the final keyword was found.
1964
     *    'is_readonly' => boolean, // TRUE if the readonly keyword was found.
1965
     *   );
1966
     * </code>
1967
     *
1968
     * @param int $stackPtr The position in the stack of the T_CLASS token to
1969
     *                      acquire the properties for.
1970
     *
1971
     * @return array
1972
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1973
     *                                                      T_CLASS token.
1974
     */
1975
    public function getClassProperties($stackPtr)
42✔
1976
    {
1977
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS) {
42✔
1978
            throw new RuntimeException('$stackPtr must be of type T_CLASS');
9✔
1979
        }
1980

1981
        $valid = [
22✔
1982
            T_FINAL      => T_FINAL,
33✔
1983
            T_ABSTRACT   => T_ABSTRACT,
33✔
1984
            T_READONLY   => T_READONLY,
33✔
1985
            T_WHITESPACE => T_WHITESPACE,
33✔
1986
            T_COMMENT    => T_COMMENT,
33✔
1987
        ];
22✔
1988

1989
        $isAbstract = false;
33✔
1990
        $isFinal    = false;
33✔
1991
        $isReadonly = false;
33✔
1992

1993
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
33✔
1994
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
33✔
1995
                break;
33✔
1996
            }
1997

1998
            switch ($this->tokens[$i]['code']) {
33✔
1999
            case T_ABSTRACT:
33✔
2000
                $isAbstract = true;
15✔
2001
                break;
15✔
2002

2003
            case T_FINAL:
33✔
2004
                $isFinal = true;
12✔
2005
                break;
12✔
2006

2007
            case T_READONLY:
33✔
2008
                $isReadonly = true;
15✔
2009
                break;
15✔
2010
            }
2011
        }//end for
2012

2013
        return [
22✔
2014
            'is_abstract' => $isAbstract,
33✔
2015
            'is_final'    => $isFinal,
33✔
2016
            'is_readonly' => $isReadonly,
33✔
2017
        ];
22✔
2018

2019
    }//end getClassProperties()
2020

2021

2022
    /**
2023
     * Determine if the passed token is a reference operator.
2024
     *
2025
     * Returns true if the specified token position represents a reference.
2026
     * Returns false if the token represents a bitwise operator.
2027
     *
2028
     * @param int $stackPtr The position of the T_BITWISE_AND token.
2029
     *
2030
     * @return boolean
2031
     */
2032
    public function isReference($stackPtr)
228✔
2033
    {
2034
        if ($this->tokens[$stackPtr]['code'] !== T_BITWISE_AND) {
228✔
2035
            return false;
9✔
2036
        }
2037

2038
        $tokenBefore = $this->findPrevious(
219✔
2039
            Tokens::$emptyTokens,
219✔
2040
            ($stackPtr - 1),
219✔
2041
            null,
219✔
2042
            true
219✔
2043
        );
146✔
2044

2045
        if ($this->tokens[$tokenBefore]['code'] === T_FUNCTION
219✔
2046
            || $this->tokens[$tokenBefore]['code'] === T_CLOSURE
216✔
2047
            || $this->tokens[$tokenBefore]['code'] === T_FN
219✔
2048
        ) {
2049
            // Function returns a reference.
2050
            return true;
9✔
2051
        }
2052

2053
        if ($this->tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
210✔
2054
            // Inside a foreach loop or array assignment, this is a reference.
2055
            return true;
18✔
2056
        }
2057

2058
        if ($this->tokens[$tokenBefore]['code'] === T_AS) {
192✔
2059
            // Inside a foreach loop, this is a reference.
2060
            return true;
3✔
2061
        }
2062

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

2069
        $tokenAfter = $this->findNext(
168✔
2070
            Tokens::$emptyTokens,
168✔
2071
            ($stackPtr + 1),
168✔
2072
            null,
168✔
2073
            true
168✔
2074
        );
112✔
2075

2076
        if ($this->tokens[$tokenAfter]['code'] === T_NEW) {
168✔
2077
            return true;
3✔
2078
        }
2079

2080
        if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === true) {
165✔
2081
            $brackets    = $this->tokens[$stackPtr]['nested_parenthesis'];
117✔
2082
            $lastBracket = array_pop($brackets);
117✔
2083
            if (isset($this->tokens[$lastBracket]['parenthesis_owner']) === true) {
117✔
2084
                $owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']];
72✔
2085
                if ($owner['code'] === T_FUNCTION
72✔
2086
                    || $owner['code'] === T_CLOSURE
60✔
2087
                    || $owner['code'] === T_FN
72✔
2088
                ) {
2089
                    $params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']);
51✔
2090
                    foreach ($params as $param) {
65✔
2091
                        if ($param['reference_token'] === $stackPtr) {
51✔
2092
                            // Function parameter declared to be passed by reference.
2093
                            return true;
36✔
2094
                        }
2095
                    }
2096
                }//end if
2097
            } else {
2098
                $prev = false;
45✔
2099
                for ($t = ($this->tokens[$lastBracket]['parenthesis_opener'] - 1); $t >= 0; $t--) {
45✔
2100
                    if ($this->tokens[$t]['code'] !== T_WHITESPACE) {
45✔
2101
                        $prev = $t;
45✔
2102
                        break;
45✔
2103
                    }
2104
                }
2105

2106
                if ($prev !== false && $this->tokens[$prev]['code'] === T_USE) {
45✔
2107
                    // Closure use by reference.
2108
                    return true;
3✔
2109
                }
2110
            }//end if
2111
        }//end if
2112

2113
        // Pass by reference in function calls and assign by reference in arrays.
2114
        if ($this->tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
126✔
2115
            || $this->tokens[$tokenBefore]['code'] === T_COMMA
111✔
2116
            || $this->tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY
126✔
2117
        ) {
2118
            if ($this->tokens[$tokenAfter]['code'] === T_VARIABLE) {
90✔
2119
                return true;
66✔
2120
            } else {
2121
                $skip   = Tokens::$emptyTokens;
24✔
2122
                $skip[] = T_NS_SEPARATOR;
24✔
2123
                $skip[] = T_SELF;
24✔
2124
                $skip[] = T_PARENT;
24✔
2125
                $skip[] = T_STATIC;
24✔
2126
                $skip[] = T_STRING;
24✔
2127
                $skip[] = T_NAMESPACE;
24✔
2128
                $skip[] = T_DOUBLE_COLON;
24✔
2129

2130
                $nextSignificantAfter = $this->findNext(
24✔
2131
                    $skip,
24✔
2132
                    ($stackPtr + 1),
24✔
2133
                    null,
24✔
2134
                    true
24✔
2135
                );
16✔
2136
                if ($this->tokens[$nextSignificantAfter]['code'] === T_VARIABLE) {
24✔
2137
                    return true;
24✔
2138
                }
2139
            }//end if
2140
        }//end if
2141

2142
        return false;
36✔
2143

2144
    }//end isReference()
2145

2146

2147
    /**
2148
     * Returns the content of the tokens from the specified start position in
2149
     * the token stack for the specified length.
2150
     *
2151
     * @param int  $start       The position to start from in the token stack.
2152
     * @param int  $length      The length of tokens to traverse from the start pos.
2153
     * @param bool $origContent Whether the original content or the tab replaced
2154
     *                          content should be used.
2155
     *
2156
     * @return string The token contents.
2157
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position does not exist.
2158
     */
2159
    public function getTokensAsString($start, $length, $origContent=false)
84✔
2160
    {
2161
        if (is_int($start) === false || isset($this->tokens[$start]) === false) {
84✔
2162
            throw new RuntimeException('The $start position for getTokensAsString() must exist in the token stack');
6✔
2163
        }
2164

2165
        if (is_int($length) === false || $length <= 0) {
78✔
2166
            return '';
9✔
2167
        }
2168

2169
        $str = '';
69✔
2170
        $end = ($start + $length);
69✔
2171
        if ($end > $this->numTokens) {
69✔
2172
            $end = $this->numTokens;
3✔
2173
        }
2174

2175
        for ($i = $start; $i < $end; $i++) {
69✔
2176
            // If tabs are being converted to spaces by the tokeniser, the
2177
            // original content should be used instead of the converted content.
2178
            if ($origContent === true && isset($this->tokens[$i]['orig_content']) === true) {
69✔
2179
                $str .= $this->tokens[$i]['orig_content'];
6✔
2180
            } else {
2181
                $str .= $this->tokens[$i]['content'];
69✔
2182
            }
2183
        }
2184

2185
        return $str;
69✔
2186

2187
    }//end getTokensAsString()
2188

2189

2190
    /**
2191
     * Returns the position of the previous specified token(s).
2192
     *
2193
     * If a value is specified, the previous token of the specified type(s)
2194
     * containing the specified value will be returned.
2195
     *
2196
     * Returns false if no token can be found.
2197
     *
2198
     * @param int|string|array $types   The type(s) of tokens to search for.
2199
     * @param int              $start   The position to start searching from in the
2200
     *                                  token stack.
2201
     * @param int|null         $end     The end position to fail if no token is found.
2202
     *                                  if not specified or null, end will default to
2203
     *                                  the start of the token stack.
2204
     * @param bool             $exclude If true, find the previous token that is NOT of
2205
     *                                  the types specified in $types.
2206
     * @param string|null      $value   The value that the token(s) must be equal to.
2207
     *                                  If value is omitted, tokens with any value will
2208
     *                                  be returned.
2209
     * @param bool             $local   If true, tokens outside the current statement
2210
     *                                  will not be checked. IE. checking will stop
2211
     *                                  at the previous semicolon found.
2212
     *
2213
     * @return int|false
2214
     * @see    findNext()
2215
     */
2216
    public function findPrevious(
×
2217
        $types,
2218
        $start,
2219
        $end=null,
2220
        $exclude=false,
2221
        $value=null,
2222
        $local=false
2223
    ) {
2224
        $types = (array) $types;
×
2225

2226
        if ($end === null) {
×
2227
            $end = 0;
×
2228
        }
2229

2230
        for ($i = $start; $i >= $end; $i--) {
×
2231
            $found = (bool) $exclude;
×
2232
            foreach ($types as $type) {
×
2233
                if ($this->tokens[$i]['code'] === $type) {
×
2234
                    $found = !$exclude;
×
2235
                    break;
×
2236
                }
2237
            }
2238

2239
            if ($found === true) {
×
2240
                if ($value === null) {
×
2241
                    return $i;
×
2242
                } else if ($this->tokens[$i]['content'] === $value) {
×
2243
                    return $i;
×
2244
                }
2245
            }
2246

2247
            if ($local === true) {
×
2248
                if (isset($this->tokens[$i]['scope_opener']) === true
×
2249
                    && $i === $this->tokens[$i]['scope_closer']
×
2250
                ) {
2251
                    $i = $this->tokens[$i]['scope_opener'];
×
2252
                } else if (isset($this->tokens[$i]['bracket_opener']) === true
×
2253
                    && $i === $this->tokens[$i]['bracket_closer']
×
2254
                ) {
2255
                    $i = $this->tokens[$i]['bracket_opener'];
×
2256
                } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
×
2257
                    && $i === $this->tokens[$i]['parenthesis_closer']
×
2258
                ) {
2259
                    $i = $this->tokens[$i]['parenthesis_opener'];
×
2260
                } else if ($this->tokens[$i]['code'] === T_SEMICOLON) {
×
2261
                    break;
×
2262
                }
2263
            }
2264
        }//end for
2265

2266
        return false;
×
2267

2268
    }//end findPrevious()
2269

2270

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

2307
        if ($end === null || $end > $this->numTokens) {
×
2308
            $end = $this->numTokens;
×
2309
        }
2310

2311
        for ($i = $start; $i < $end; $i++) {
×
2312
            $found = (bool) $exclude;
×
2313
            foreach ($types as $type) {
×
2314
                if ($this->tokens[$i]['code'] === $type) {
×
2315
                    $found = !$exclude;
×
2316
                    break;
×
2317
                }
2318
            }
2319

2320
            if ($found === true) {
×
2321
                if ($value === null) {
×
2322
                    return $i;
×
2323
                } else if ($this->tokens[$i]['content'] === $value) {
×
2324
                    return $i;
×
2325
                }
2326
            }
2327

2328
            if ($local === true && $this->tokens[$i]['code'] === T_SEMICOLON) {
×
2329
                break;
×
2330
            }
2331
        }//end for
2332

2333
        return false;
×
2334

2335
    }//end findNext()
2336

2337

2338
    /**
2339
     * Returns the position of the first non-whitespace token in a statement.
2340
     *
2341
     * @param int              $start  The position to start searching from in the token stack.
2342
     * @param int|string|array $ignore Token types that should not be considered stop points.
2343
     *
2344
     * @return int
2345
     */
2346
    public function findStartOfStatement($start, $ignore=null)
210✔
2347
    {
2348
        $startTokens = Tokens::$blockOpeners;
210✔
2349
        $startTokens[T_OPEN_SHORT_ARRAY]   = true;
210✔
2350
        $startTokens[T_OPEN_TAG]           = true;
210✔
2351
        $startTokens[T_OPEN_TAG_WITH_ECHO] = true;
210✔
2352

2353
        $endTokens = [
140✔
2354
            T_CLOSE_TAG    => true,
210✔
2355
            T_COLON        => true,
210✔
2356
            T_COMMA        => true,
210✔
2357
            T_DOUBLE_ARROW => true,
210✔
2358
            T_MATCH_ARROW  => true,
210✔
2359
            T_SEMICOLON    => true,
210✔
2360
        ];
140✔
2361

2362
        if ($ignore !== null) {
210✔
2363
            $ignore = (array) $ignore;
×
2364
            foreach ($ignore as $code) {
×
2365
                if (isset($startTokens[$code]) === true) {
×
2366
                    unset($startTokens[$code]);
×
2367
                }
2368

2369
                if (isset($endTokens[$code]) === true) {
×
2370
                    unset($endTokens[$code]);
×
2371
                }
2372
            }
2373
        }
2374

2375
        // If the start token is inside the case part of a match expression,
2376
        // find the start of the condition. If it's in the statement part, find
2377
        // the token that comes after the match arrow.
2378
        if (empty($this->tokens[$start]['conditions']) === false) {
210✔
2379
            $conditions         = $this->tokens[$start]['conditions'];
162✔
2380
            $lastConditionOwner = end($conditions);
162✔
2381
            $matchExpression    = key($conditions);
162✔
2382

2383
            if ($lastConditionOwner === T_MATCH
162✔
2384
                // Check if the $start token is at the same parentheses nesting level as the match token.
2385
                && ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === true
139✔
2386
                && empty($this->tokens[$start]['nested_parenthesis']) === true)
131✔
2387
                || ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === false
125✔
2388
                && empty($this->tokens[$start]['nested_parenthesis']) === false)
125✔
2389
                && $this->tokens[$matchExpression]['nested_parenthesis'] === $this->tokens[$start]['nested_parenthesis']))
162✔
2390
            ) {
2391
                // Walk back to the previous match arrow (if it exists).
2392
                $lastComma          = null;
45✔
2393
                $inNestedExpression = false;
45✔
2394
                for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
45✔
2395
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_MATCH_ARROW) {
45✔
2396
                        break;
33✔
2397
                    }
2398

2399
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_COMMA) {
45✔
2400
                        $lastComma = $prevMatch;
24✔
2401
                        continue;
24✔
2402
                    }
2403

2404
                    // Skip nested statements.
2405
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
45✔
2406
                        && $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
45✔
2407
                    ) {
2408
                        $prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
12✔
2409
                        continue;
12✔
2410
                    }
2411

2412
                    if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
45✔
2413
                        && $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
45✔
2414
                    ) {
2415
                        $prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
15✔
2416
                        continue;
15✔
2417
                    }
2418

2419
                    // Stop if we're _within_ a nested short array statement, which may contain comma's too.
2420
                    // No need to deal with parentheses, those are handled above via the `nested_parenthesis` checks.
2421
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
45✔
2422
                        && $this->tokens[$prevMatch]['bracket_closer'] > $start
45✔
2423
                    ) {
2424
                        $inNestedExpression = true;
15✔
2425
                        break;
15✔
2426
                    }
2427
                }//end for
2428

2429
                if ($inNestedExpression === false) {
45✔
2430
                    // $prevMatch will now either be the scope opener or a match arrow.
2431
                    // If it is the scope opener, go the first non-empty token after. $start will have been part of the first condition.
2432
                    if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) {
33✔
2433
                        // We're before the arrow in the first case.
2434
                        $next = $this->findNext(Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
12✔
2435
                        if ($next === false) {
12✔
2436
                            // Shouldn't be possible.
2437
                            return $start;
×
2438
                        }
2439

2440
                        return $next;
12✔
2441
                    }
2442

2443
                    // Okay, so we found a match arrow.
2444
                    // If $start was part of the "next" condition, the last comma will be set.
2445
                    // Otherwise, $start must have been part of a return expression.
2446
                    if (isset($lastComma) === true && $lastComma > $prevMatch) {
33✔
2447
                        $prevMatch = $lastComma;
15✔
2448
                    }
2449

2450
                    // In both cases, go to the first non-empty token after.
2451
                    $next = $this->findNext(Tokens::$emptyTokens, ($prevMatch + 1), null, true);
33✔
2452
                    if ($next === false) {
33✔
2453
                        // Shouldn't be possible.
2454
                        return $start;
×
2455
                    }
2456

2457
                    return $next;
33✔
2458
                }//end if
2459
            }//end if
2460
        }//end if
2461

2462
        $lastNotEmpty = $start;
180✔
2463

2464
        // If we are starting at a token that ends a scope block, skip to
2465
        // the start and continue from there.
2466
        // If we are starting at a token that ends a statement, skip this
2467
        // token so we find the true start of the statement.
2468
        while (isset($endTokens[$this->tokens[$start]['code']]) === true
180✔
2469
            || (isset($this->tokens[$start]['scope_condition']) === true
180✔
2470
            && $start === $this->tokens[$start]['scope_closer'])
180✔
2471
        ) {
2472
            if (isset($this->tokens[$start]['scope_condition']) === true) {
51✔
2473
                $start = $this->tokens[$start]['scope_condition'];
27✔
2474
            } else {
2475
                $start--;
30✔
2476
            }
2477
        }
2478

2479
        for ($i = $start; $i >= 0; $i--) {
180✔
2480
            if (isset($startTokens[$this->tokens[$i]['code']]) === true
180✔
2481
                || isset($endTokens[$this->tokens[$i]['code']]) === true
180✔
2482
            ) {
2483
                // Found the end of the previous statement.
2484
                return $lastNotEmpty;
180✔
2485
            }
2486

2487
            if (isset($this->tokens[$i]['scope_opener']) === true
177✔
2488
                && $i === $this->tokens[$i]['scope_closer']
177✔
2489
                && $this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS
177✔
2490
                && $this->tokens[$i]['code'] !== T_END_NOWDOC
177✔
2491
                && $this->tokens[$i]['code'] !== T_END_HEREDOC
177✔
2492
                && $this->tokens[$i]['code'] !== T_BREAK
177✔
2493
                && $this->tokens[$i]['code'] !== T_RETURN
177✔
2494
                && $this->tokens[$i]['code'] !== T_CONTINUE
177✔
2495
                && $this->tokens[$i]['code'] !== T_THROW
177✔
2496
                && $this->tokens[$i]['code'] !== T_EXIT
177✔
2497
                && $this->tokens[$i]['code'] !== T_GOTO
177✔
2498
            ) {
2499
                // Found the end of the previous scope block.
2500
                return $lastNotEmpty;
3✔
2501
            }
2502

2503
            // Skip nested statements.
2504
            if (isset($this->tokens[$i]['bracket_opener']) === true
177✔
2505
                && $i === $this->tokens[$i]['bracket_closer']
177✔
2506
            ) {
2507
                $i = $this->tokens[$i]['bracket_opener'];
3✔
2508
            } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
177✔
2509
                && $i === $this->tokens[$i]['parenthesis_closer']
177✔
2510
            ) {
2511
                $i = $this->tokens[$i]['parenthesis_opener'];
21✔
2512
            } else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
177✔
2513
                $start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
6✔
2514
                if ($start !== false) {
6✔
2515
                    $i = $start;
6✔
2516
                }
2517
            }//end if
2518

2519
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
177✔
2520
                $lastNotEmpty = $i;
177✔
2521
            }
2522
        }//end for
2523

2524
        return 0;
×
2525

2526
    }//end findStartOfStatement()
2527

2528

2529
    /**
2530
     * Returns the position of the last non-whitespace token in a statement.
2531
     *
2532
     * @param int              $start  The position to start searching from in the token stack.
2533
     * @param int|string|array $ignore Token types that should not be considered stop points.
2534
     *
2535
     * @return int
2536
     */
2537
    public function findEndOfStatement($start, $ignore=null)
66✔
2538
    {
2539
        $endTokens = [
44✔
2540
            T_COLON                => true,
66✔
2541
            T_COMMA                => true,
66✔
2542
            T_DOUBLE_ARROW         => true,
66✔
2543
            T_SEMICOLON            => true,
66✔
2544
            T_CLOSE_PARENTHESIS    => true,
66✔
2545
            T_CLOSE_SQUARE_BRACKET => true,
66✔
2546
            T_CLOSE_CURLY_BRACKET  => true,
66✔
2547
            T_CLOSE_SHORT_ARRAY    => true,
66✔
2548
            T_OPEN_TAG             => true,
66✔
2549
            T_CLOSE_TAG            => true,
66✔
2550
        ];
44✔
2551

2552
        if ($ignore !== null) {
66✔
2553
            $ignore = (array) $ignore;
×
2554
            foreach ($ignore as $code) {
×
2555
                unset($endTokens[$code]);
×
2556
            }
2557
        }
2558

2559
        // If the start token is inside the case part of a match expression,
2560
        // advance to the match arrow and continue looking for the
2561
        // end of the statement from there so that we skip over commas.
2562
        if ($this->tokens[$start]['code'] !== T_MATCH_ARROW) {
66✔
2563
            $matchExpression = $this->getCondition($start, T_MATCH);
66✔
2564
            if ($matchExpression !== false) {
66✔
2565
                $beforeArrow    = true;
30✔
2566
                $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']);
30✔
2567
                if ($prevMatchArrow !== false) {
30✔
2568
                    $prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start);
27✔
2569
                    if ($prevComma === false) {
27✔
2570
                        // No comma between this token and the last match arrow,
2571
                        // so this token exists after the arrow and we can continue
2572
                        // checking as normal.
2573
                        $beforeArrow = false;
12✔
2574
                    }
2575
                }
2576

2577
                if ($beforeArrow === true) {
30✔
2578
                    $nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
30✔
2579
                    if ($nextMatchArrow !== false) {
30✔
2580
                        $start = $nextMatchArrow;
30✔
2581
                    }
2582
                }
2583
            }//end if
2584
        }//end if
2585

2586
        $lastNotEmpty = $start;
66✔
2587
        for ($i = $start; $i < $this->numTokens; $i++) {
66✔
2588
            if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
66✔
2589
                // Found the end of the statement.
2590
                if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS
60✔
2591
                    || $this->tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
57✔
2592
                    || $this->tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
57✔
2593
                    || $this->tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
51✔
2594
                    || $this->tokens[$i]['code'] === T_OPEN_TAG
48✔
2595
                    || $this->tokens[$i]['code'] === T_CLOSE_TAG
60✔
2596
                ) {
2597
                    return $lastNotEmpty;
24✔
2598
                }
2599

2600
                return $i;
48✔
2601
            }
2602

2603
            // Skip nested statements.
2604
            if (isset($this->tokens[$i]['scope_closer']) === true
66✔
2605
                && ($i === $this->tokens[$i]['scope_opener']
56✔
2606
                || $i === $this->tokens[$i]['scope_condition'])
66✔
2607
            ) {
2608
                if ($this->tokens[$i]['code'] === T_FN) {
36✔
2609
                    $lastNotEmpty = $this->tokens[$i]['scope_closer'];
18✔
2610
                    $i            = ($this->tokens[$i]['scope_closer'] - 1);
18✔
2611
                    continue;
18✔
2612
                }
2613

2614
                if ($i === $start && isset(Tokens::$scopeOpeners[$this->tokens[$i]['code']]) === true) {
21✔
2615
                    return $this->tokens[$i]['scope_closer'];
9✔
2616
                }
2617

2618
                $i = $this->tokens[$i]['scope_closer'];
15✔
2619
            } else if (isset($this->tokens[$i]['bracket_closer']) === true
48✔
2620
                && $i === $this->tokens[$i]['bracket_opener']
48✔
2621
            ) {
2622
                $i = $this->tokens[$i]['bracket_closer'];
6✔
2623
            } else if (isset($this->tokens[$i]['parenthesis_closer']) === true
48✔
2624
                && $i === $this->tokens[$i]['parenthesis_opener']
48✔
2625
            ) {
2626
                $i = $this->tokens[$i]['parenthesis_closer'];
9✔
2627
            } else if ($this->tokens[$i]['code'] === T_OPEN_USE_GROUP) {
48✔
2628
                $end = $this->findNext(T_CLOSE_USE_GROUP, ($i + 1));
6✔
2629
                if ($end !== false) {
6✔
2630
                    $i = $end;
6✔
2631
                }
2632
            }//end if
2633

2634
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
48✔
2635
                $lastNotEmpty = $i;
48✔
2636
            }
2637
        }//end for
2638

2639
        return ($this->numTokens - 1);
3✔
2640

2641
    }//end findEndOfStatement()
2642

2643

2644
    /**
2645
     * Returns the position of the first token on a line, matching given type.
2646
     *
2647
     * Returns false if no token can be found.
2648
     *
2649
     * @param int|string|array $types   The type(s) of tokens to search for.
2650
     * @param int              $start   The position to start searching from in the
2651
     *                                  token stack.
2652
     * @param bool             $exclude If true, find the token that is NOT of
2653
     *                                  the types specified in $types.
2654
     * @param string           $value   The value that the token must be equal to.
2655
     *                                  If value is omitted, tokens with any value will
2656
     *                                  be returned.
2657
     *
2658
     * @return int|false The first token which matches on the line containing the start
2659
     *                   token, between the start of the line and the start token.
2660
     *                   Note: The first token matching might be the start token.
2661
     *                   FALSE when no matching token could be found between the start of
2662
     *                   the line and the start token.
2663
     */
2664
    public function findFirstOnLine($types, $start, $exclude=false, $value=null)
×
2665
    {
2666
        if (is_array($types) === false) {
×
2667
            $types = [$types];
×
2668
        }
2669

2670
        $foundToken = false;
×
2671

2672
        for ($i = $start; $i >= 0; $i--) {
×
2673
            if ($this->tokens[$i]['line'] < $this->tokens[$start]['line']) {
×
2674
                break;
×
2675
            }
2676

2677
            $found = $exclude;
×
2678
            foreach ($types as $type) {
×
2679
                if ($exclude === false) {
×
2680
                    if ($this->tokens[$i]['code'] === $type) {
×
2681
                        $found = true;
×
2682
                        break;
×
2683
                    }
2684
                } else {
2685
                    if ($this->tokens[$i]['code'] === $type) {
×
2686
                        $found = false;
×
2687
                        break;
×
2688
                    }
2689
                }
2690
            }
2691

2692
            if ($found === true) {
×
2693
                if ($value === null) {
×
2694
                    $foundToken = $i;
×
2695
                } else if ($this->tokens[$i]['content'] === $value) {
×
2696
                    $foundToken = $i;
×
2697
                }
2698
            }
2699
        }//end for
2700

2701
        return $foundToken;
×
2702

2703
    }//end findFirstOnLine()
2704

2705

2706
    /**
2707
     * Determine if the passed token has a condition of one of the passed types.
2708
     *
2709
     * @param int              $stackPtr The position of the token we are checking.
2710
     * @param int|string|array $types    The type(s) of tokens to search for.
2711
     *
2712
     * @return boolean
2713
     */
2714
    public function hasCondition($stackPtr, $types)
21✔
2715
    {
2716
        // Check for the existence of the token.
2717
        if (isset($this->tokens[$stackPtr]) === false) {
21✔
2718
            return false;
3✔
2719
        }
2720

2721
        // Make sure the token has conditions.
2722
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
18✔
2723
            return false;
3✔
2724
        }
2725

2726
        $types      = (array) $types;
15✔
2727
        $conditions = $this->tokens[$stackPtr]['conditions'];
15✔
2728

2729
        foreach ($types as $type) {
15✔
2730
            if (in_array($type, $conditions, true) === true) {
15✔
2731
                // We found a token with the required type.
2732
                return true;
15✔
2733
            }
2734
        }
2735

2736
        return false;
15✔
2737

2738
    }//end hasCondition()
2739

2740

2741
    /**
2742
     * Return the position of the condition for the passed token.
2743
     *
2744
     * Returns FALSE if the token does not have the condition.
2745
     *
2746
     * @param int        $stackPtr The position of the token we are checking.
2747
     * @param int|string $type     The type of token to search for.
2748
     * @param bool       $first    If TRUE, will return the matched condition
2749
     *                             furthest away from the passed token.
2750
     *                             If FALSE, will return the matched condition
2751
     *                             closest to the passed token.
2752
     *
2753
     * @return int|false
2754
     */
2755
    public function getCondition($stackPtr, $type, $first=true)
30✔
2756
    {
2757
        // Check for the existence of the token.
2758
        if (isset($this->tokens[$stackPtr]) === false) {
30✔
2759
            return false;
3✔
2760
        }
2761

2762
        // Make sure the token has conditions.
2763
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
27✔
2764
            return false;
3✔
2765
        }
2766

2767
        $conditions = $this->tokens[$stackPtr]['conditions'];
24✔
2768
        if ($first === false) {
24✔
2769
            $conditions = array_reverse($conditions, true);
12✔
2770
        }
2771

2772
        foreach ($conditions as $token => $condition) {
24✔
2773
            if ($condition === $type) {
24✔
2774
                return $token;
24✔
2775
            }
2776
        }
2777

2778
        return false;
24✔
2779

2780
    }//end getCondition()
2781

2782

2783
    /**
2784
     * Returns the name of the class that the specified class extends.
2785
     * (works for classes, anonymous classes and interfaces)
2786
     *
2787
     * Returns FALSE on error or if there is no extended class name.
2788
     *
2789
     * @param int $stackPtr The stack position of the class.
2790
     *
2791
     * @return string|false
2792
     */
2793
    public function findExtendedClassName($stackPtr)
51✔
2794
    {
2795
        // Check for the existence of the token.
2796
        if (isset($this->tokens[$stackPtr]) === false) {
51✔
2797
            return false;
3✔
2798
        }
2799

2800
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
48✔
2801
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
48✔
2802
            && $this->tokens[$stackPtr]['code'] !== T_INTERFACE
48✔
2803
        ) {
2804
            return false;
3✔
2805
        }
2806

2807
        if (isset($this->tokens[$stackPtr]['scope_opener']) === false) {
45✔
2808
            return false;
3✔
2809
        }
2810

2811
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
42✔
2812
        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex);
42✔
2813
        if ($extendsIndex === false) {
42✔
2814
            return false;
9✔
2815
        }
2816

2817
        $find = [
22✔
2818
            T_NS_SEPARATOR,
33✔
2819
            T_STRING,
33✔
2820
            T_WHITESPACE,
33✔
2821
        ];
22✔
2822

2823
        $end  = $this->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true);
33✔
2824
        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
33✔
2825
        $name = trim($name);
33✔
2826

2827
        if ($name === '') {
33✔
2828
            return false;
3✔
2829
        }
2830

2831
        return $name;
30✔
2832

2833
    }//end findExtendedClassName()
2834

2835

2836
    /**
2837
     * Returns the names of the interfaces that the specified class or enum implements.
2838
     *
2839
     * Returns FALSE on error or if there are no implemented interface names.
2840
     *
2841
     * @param int $stackPtr The stack position of the class or enum token.
2842
     *
2843
     * @return array|false
2844
     */
2845
    public function findImplementedInterfaceNames($stackPtr)
48✔
2846
    {
2847
        // Check for the existence of the token.
2848
        if (isset($this->tokens[$stackPtr]) === false) {
48✔
2849
            return false;
3✔
2850
        }
2851

2852
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
45✔
2853
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
45✔
2854
            && $this->tokens[$stackPtr]['code'] !== T_ENUM
45✔
2855
        ) {
2856
            return false;
6✔
2857
        }
2858

2859
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
39✔
2860
            return false;
3✔
2861
        }
2862

2863
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
36✔
2864
        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
36✔
2865
        if ($implementsIndex === false) {
36✔
2866
            return false;
6✔
2867
        }
2868

2869
        $find = [
20✔
2870
            T_NS_SEPARATOR,
30✔
2871
            T_STRING,
30✔
2872
            T_WHITESPACE,
30✔
2873
            T_COMMA,
30✔
2874
        ];
20✔
2875

2876
        $end  = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
30✔
2877
        $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
30✔
2878
        $name = trim($name);
30✔
2879

2880
        if ($name === '') {
30✔
2881
            return false;
3✔
2882
        } else {
2883
            $names = explode(',', $name);
27✔
2884
            $names = array_map('trim', $names);
27✔
2885
            return $names;
27✔
2886
        }
2887

2888
    }//end findImplementedInterfaceNames()
2889

2890

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