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

PHPCSStandards / PHP_CodeSniffer / 17174734458

23 Aug 2025 11:06AM UTC coverage: 76.88% (-2.1%) from 78.934%
17174734458

push

github

jrfnl
TEMP/TESTING PHPUnit 6331

19187 of 24957 relevant lines covered (76.88%)

60.25 hits per line

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

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

10
namespace PHP_CodeSniffer\Files;
11

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

22
class File
23
{
24

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

155
    /**
156
     * The original total number of errors that can be fixed (first run on a file).
157
     *
158
     * {@internal This should be regarded as an immutable property.}
159
     *
160
     * @var integer
161
     */
162
    private $fixableErrorCountFirstRun;
163

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

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

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

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

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

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

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

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

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

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

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

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

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

269

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

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

296
    }//end __construct()
297

298

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

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

320
    }//end setContent()
321

322

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

334
    }//end reloadContent()
×
335

336

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

346
    }//end disableCaching()
347

348

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

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

367
        $this->parse();
×
368

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

567
    }//end process()
568

569

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

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

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

597
            return;
×
598
        }
599

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

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

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

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

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

631
    }//end parse()
632

633

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

643
    }//end getTokens()
644

645

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

662
    }//end cleanUp()
663

664

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

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

696
    }//end addError()
697

698

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

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

730
    }//end addWarning()
731

732

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

754
    }//end addErrorOnLine()
755

756

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

778
    }//end addWarningOnLine()
779

780

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

807
        return false;
×
808

809
    }//end addFixableError()
810

811

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

838
        return false;
×
839

840
    }//end addFixableWarning()
841

842

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

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

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

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

895
        if (isset($this->tokenizer->ignoredLines[$line]) === true && $this->tokenizer->ignoredLines[$line]->isIgnored($sniffCode) === true) {
136✔
896
            return false;
72✔
897
        }
898

899
        $includeAll = true;
118✔
900
        if ($this->configCache['cache'] === false
118✔
901
            || $this->configCache['recordErrors'] === false
118✔
902
        ) {
903
            $includeAll = false;
118✔
904
        }
905

906
        // Filter out any messages for sniffs that shouldn't have run
907
        // due to the use of the --sniffs or --exclude command line argument,
908
        // but don't filter out "Internal" messages.
909
        if ($includeAll === false
118✔
910
            && (($parts[0] !== 'Internal'
118✔
911
            && empty($this->configCache['sniffs']) === false
118✔
912
            && in_array(strtolower($listenerCode), $this->configCache['sniffs'], true) === false)
118✔
913
            || (empty($this->configCache['exclude']) === false
118✔
914
            && in_array(strtolower($listenerCode), $this->configCache['exclude'], true) === true))
118✔
915
        ) {
916
            return false;
×
917
        }
918

919
        // If we know this sniff code is being ignored for this file, return early.
920
        foreach ($checkCodes as $checkCode) {
118✔
921
            if (isset($this->ignoredCodes[$checkCode]) === true) {
118✔
922
                return false;
×
923
            }
924
        }
925

926
        $oppositeType = 'warning';
118✔
927
        if ($error === false) {
118✔
928
            $oppositeType = 'error';
50✔
929
        }
930

931
        foreach ($checkCodes as $checkCode) {
118✔
932
            // Make sure this message type has not been set to the opposite message type.
933
            if (isset($this->ruleset->ruleset[$checkCode]['type']) === true
118✔
934
                && $this->ruleset->ruleset[$checkCode]['type'] === $oppositeType
118✔
935
            ) {
936
                $error = !$error;
×
937
                break;
×
938
            }
939
        }
940

941
        if ($error === true) {
118✔
942
            $configSeverity = $this->configCache['errorSeverity'];
102✔
943
            $messageCount   = &$this->errorCount;
102✔
944
            $messages       = &$this->errors;
102✔
945
        } else {
946
            $configSeverity = $this->configCache['warningSeverity'];
50✔
947
            $messageCount   = &$this->warningCount;
50✔
948
            $messages       = &$this->warnings;
50✔
949
        }
950

951
        if ($includeAll === false && $configSeverity === 0) {
118✔
952
            // Don't bother doing any processing as these messages are just going to
953
            // be hidden in the reports anyway.
954
            return false;
×
955
        }
956

957
        if ($severity === 0) {
118✔
958
            $severity = 5;
118✔
959
        }
960

961
        foreach ($checkCodes as $checkCode) {
118✔
962
            // Make sure we are interested in this severity level.
963
            if (isset($this->ruleset->ruleset[$checkCode]['severity']) === true) {
118✔
964
                $severity = $this->ruleset->ruleset[$checkCode]['severity'];
6✔
965
                break;
6✔
966
            }
967
        }
968

969
        if ($includeAll === false && $configSeverity > $severity) {
118✔
970
            return false;
6✔
971
        }
972

973
        // Make sure we are not ignoring this file.
974
        $included = null;
112✔
975
        if (trim($this->path, '\'"') === 'STDIN') {
112✔
976
            $included = true;
112✔
977
        } else {
978
            foreach ($checkCodes as $checkCode) {
×
979
                $patterns = null;
×
980

981
                if (isset($this->configCache['includePatterns'][$checkCode]) === true) {
×
982
                    $patterns  = $this->configCache['includePatterns'][$checkCode];
×
983
                    $excluding = false;
×
984
                } else if (isset($this->configCache['ignorePatterns'][$checkCode]) === true) {
×
985
                    $patterns  = $this->configCache['ignorePatterns'][$checkCode];
×
986
                    $excluding = true;
×
987
                }
988

989
                if ($patterns === null) {
×
990
                    continue;
×
991
                }
992

993
                foreach ($patterns as $pattern => $type) {
×
994
                    // While there is support for a type of each pattern
995
                    // (absolute or relative) we don't actually support it here.
996
                    $replacements = [
×
997
                        '\\,' => ',',
×
998
                        '*'   => '.*',
×
999
                    ];
×
1000

1001
                    // We assume a / directory separator, as do the exclude rules
1002
                    // most developers write, so we need a special case for any system
1003
                    // that is different.
1004
                    if (DIRECTORY_SEPARATOR === '\\') {
×
1005
                        $replacements['/'] = '\\\\';
×
1006
                    }
1007

1008
                    $pattern = '`'.strtr($pattern, $replacements).'`i';
×
1009
                    $matched = preg_match($pattern, $this->path);
×
1010

1011
                    if ($matched === 0) {
×
1012
                        if ($excluding === false && $included === null) {
×
1013
                            // This file path is not being included.
1014
                            $included = false;
×
1015
                        }
1016

1017
                        continue;
×
1018
                    }
1019

1020
                    if ($excluding === true) {
×
1021
                        // This file path is being excluded.
1022
                        $this->ignoredCodes[$checkCode] = true;
×
1023
                        return false;
×
1024
                    }
1025

1026
                    // This file path is being included.
1027
                    $included = true;
×
1028
                    break;
×
1029
                }//end foreach
1030
            }//end foreach
1031
        }//end if
1032

1033
        if ($included === false) {
112✔
1034
            // There were include rules set, but this file
1035
            // path didn't match any of them.
1036
            return false;
×
1037
        }
1038

1039
        $messageCount++;
112✔
1040
        if ($fixable === true) {
112✔
1041
            if ($error === true) {
90✔
1042
                $this->fixableErrorCount++;
90✔
1043
            } else {
1044
                $this->fixableWarningCount++;
×
1045
            }
1046
        }
1047

1048
        if ($this->configCache['recordErrors'] === false
112✔
1049
            && $includeAll === false
112✔
1050
        ) {
1051
            return true;
×
1052
        }
1053

1054
        // See if there is a custom error message format to use.
1055
        // But don't do this if we are replaying errors because replayed
1056
        // errors have already used the custom format and have had their
1057
        // data replaced.
1058
        if ($this->replayingErrors === false
112✔
1059
            && isset($this->ruleset->ruleset[$sniffCode]['message']) === true
112✔
1060
        ) {
1061
            $message = $this->ruleset->ruleset[$sniffCode]['message'];
×
1062
        }
1063

1064
        if (empty($data) === false) {
112✔
1065
            $message = vsprintf($message, $data);
104✔
1066
        }
1067

1068
        if (isset($messages[$line]) === false) {
112✔
1069
            $messages[$line] = [];
112✔
1070
        }
1071

1072
        if (isset($messages[$line][$column]) === false) {
112✔
1073
            $messages[$line][$column] = [];
112✔
1074
        }
1075

1076
        $messages[$line][$column][] = [
112✔
1077
            'message'  => $message,
112✔
1078
            'source'   => $sniffCode,
112✔
1079
            'listener' => $this->activeListener,
112✔
1080
            'severity' => $severity,
112✔
1081
            'fixable'  => $fixable,
112✔
1082
        ];
112✔
1083

1084
        if (PHP_CODESNIFFER_VERBOSITY > 1
112✔
1085
            && $this->fixer->enabled === true
112✔
1086
            && $fixable === true
112✔
1087
        ) {
1088
            StatusWriter::forceWrite("E: [Line $line] $message ($sniffCode)", 1);
×
1089
        }
1090

1091
        return true;
112✔
1092

1093
    }//end addMessage()
1094

1095

1096
    /**
1097
     * Record a metric about the file being examined.
1098
     *
1099
     * @param int    $stackPtr The stack position where the metric was recorded.
1100
     * @param string $metric   The name of the metric being recorded.
1101
     * @param string $value    The value of the metric being recorded.
1102
     *
1103
     * @return boolean
1104
     */
1105
    public function recordMetric($stackPtr, $metric, $value)
×
1106
    {
1107
        if (isset($this->metrics[$metric]) === false) {
×
1108
            $this->metrics[$metric] = ['values' => [$value => 1]];
×
1109
            $this->metricTokens[$metric][$stackPtr] = true;
×
1110
        } else if (isset($this->metricTokens[$metric][$stackPtr]) === false) {
×
1111
            $this->metricTokens[$metric][$stackPtr] = true;
×
1112
            if (isset($this->metrics[$metric]['values'][$value]) === false) {
×
1113
                $this->metrics[$metric]['values'][$value] = 1;
×
1114
            } else {
1115
                $this->metrics[$metric]['values'][$value]++;
×
1116
            }
1117
        }
1118

1119
        return true;
×
1120

1121
    }//end recordMetric()
1122

1123

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

1133
    }//end getErrorCount()
1134

1135

1136
    /**
1137
     * Returns the number of warnings raised.
1138
     *
1139
     * @return int
1140
     */
1141
    public function getWarningCount()
×
1142
    {
1143
        return $this->warningCount;
×
1144

1145
    }//end getWarningCount()
1146

1147

1148
    /**
1149
     * Returns the number of fixable errors/warnings raised.
1150
     *
1151
     * @return int
1152
     */
1153
    public function getFixableCount()
×
1154
    {
1155
        return ($this->fixableErrorCount + $this->fixableWarningCount);
×
1156

1157
    }//end getFixableCount()
1158

1159

1160
    /**
1161
     * Returns the number of fixable errors raised.
1162
     *
1163
     * @return int
1164
     */
1165
    public function getFixableErrorCount()
×
1166
    {
1167
        return $this->fixableErrorCount;
×
1168

1169
    }//end getFixableErrorCount()
1170

1171

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

1181
    }//end getFixableWarningCount()
1182

1183

1184
    /**
1185
     * Returns the actual number of fixed errors/warnings.
1186
     *
1187
     * @return int
1188
     */
1189
    public function getFixedCount()
×
1190
    {
1191
        return $this->fixedCount;
×
1192

1193
    }//end getFixedCount()
1194

1195

1196
    /**
1197
     * Returns the effective number of fixed errors.
1198
     *
1199
     * @return int
1200
     */
1201
    public function getFixedErrorCount()
×
1202
    {
1203
        return $this->fixedErrorCount;
×
1204

1205
    }//end getFixedErrorCount()
1206

1207

1208
    /**
1209
     * Returns the effective number of fixed warnings.
1210
     *
1211
     * @return int
1212
     */
1213
    public function getFixedWarningCount()
×
1214
    {
1215
        return $this->fixedWarningCount;
×
1216

1217
    }//end getFixedWarningCount()
1218

1219

1220
    /**
1221
     * Returns the list of ignored lines.
1222
     *
1223
     * @return array
1224
     */
1225
    public function getIgnoredLines()
×
1226
    {
1227
        return $this->tokenizer->ignoredLines;
×
1228

1229
    }//end getIgnoredLines()
1230

1231

1232
    /**
1233
     * Returns the errors raised from processing this file.
1234
     *
1235
     * @return array
1236
     */
1237
    public function getErrors()
×
1238
    {
1239
        return $this->errors;
×
1240

1241
    }//end getErrors()
1242

1243

1244
    /**
1245
     * Returns the warnings raised from processing this file.
1246
     *
1247
     * @return array
1248
     */
1249
    public function getWarnings()
×
1250
    {
1251
        return $this->warnings;
×
1252

1253
    }//end getWarnings()
1254

1255

1256
    /**
1257
     * Returns the metrics found while processing this file.
1258
     *
1259
     * @return array
1260
     */
1261
    public function getMetrics()
×
1262
    {
1263
        return $this->metrics;
×
1264

1265
    }//end getMetrics()
1266

1267

1268
    /**
1269
     * Returns the time taken processing this file for each invoked sniff.
1270
     *
1271
     * @return array
1272
     */
1273
    public function getListenerTimes()
×
1274
    {
1275
        return $this->listenerTimes;
×
1276

1277
    }//end getListenerTimes()
1278

1279

1280
    /**
1281
     * Returns the absolute filename of this file.
1282
     *
1283
     * @return string
1284
     */
1285
    public function getFilename()
×
1286
    {
1287
        return $this->path;
×
1288

1289
    }//end getFilename()
1290

1291

1292
    /**
1293
     * Returns the declaration name for classes, interfaces, traits, enums, and functions.
1294
     *
1295
     * @param int $stackPtr The position of the declaration token which
1296
     *                      declared the class, interface, trait, or function.
1297
     *
1298
     * @return string The name of the class, interface, trait, or function or an empty string
1299
     *                if the name could not be determined (live coding).
1300
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type
1301
     *                                                      T_FUNCTION, T_CLASS, T_TRAIT, T_ENUM, or T_INTERFACE.
1302
     */
1303
    public function getDeclarationName($stackPtr)
58✔
1304
    {
1305
        $tokenCode = $this->tokens[$stackPtr]['code'];
58✔
1306

1307
        if ($tokenCode !== T_FUNCTION
58✔
1308
            && $tokenCode !== T_CLASS
58✔
1309
            && $tokenCode !== T_INTERFACE
58✔
1310
            && $tokenCode !== T_TRAIT
58✔
1311
            && $tokenCode !== T_ENUM
58✔
1312
        ) {
1313
            throw new RuntimeException('Token type "'.$this->tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM');
12✔
1314
        }
1315

1316
        $stopPoint = $this->numTokens;
46✔
1317
        if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === true) {
46✔
1318
            // For functions, stop searching at the parenthesis opener.
1319
            $stopPoint = $this->tokens[$stackPtr]['parenthesis_opener'];
24✔
1320
        } else if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
22✔
1321
            // For OO tokens, stop searching at the open curly.
1322
            $stopPoint = $this->tokens[$stackPtr]['scope_opener'];
20✔
1323
        }
1324

1325
        $content = '';
46✔
1326
        for ($i = $stackPtr; $i < $stopPoint; $i++) {
46✔
1327
            if ($this->tokens[$i]['code'] === T_STRING) {
46✔
1328
                $content = $this->tokens[$i]['content'];
42✔
1329
                break;
42✔
1330
            }
1331
        }
1332

1333
        return $content;
46✔
1334

1335
    }//end getDeclarationName()
1336

1337

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

1404
        if ($this->tokens[$stackPtr]['code'] === T_USE
154✔
1405
            && isset($this->tokens[$stackPtr]['parenthesis_owner']) === false
154✔
1406
        ) {
1407
            throw new RuntimeException('$stackPtr was not a valid T_USE');
8✔
1408
        }
1409

1410
        if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === false) {
146✔
1411
            // Live coding or syntax error, so no params to find.
1412
            return [];
2✔
1413
        }
1414

1415
        $opener = $this->tokens[$stackPtr]['parenthesis_opener'];
144✔
1416

1417
        if (isset($this->tokens[$opener]['parenthesis_closer']) === false) {
144✔
1418
            // Live coding or syntax error, so no params to find.
1419
            return [];
4✔
1420
        }
1421

1422
        $closer = $this->tokens[$opener]['parenthesis_closer'];
140✔
1423

1424
        $vars            = [];
140✔
1425
        $currVar         = null;
140✔
1426
        $paramStart      = ($opener + 1);
140✔
1427
        $defaultStart    = null;
140✔
1428
        $equalToken      = null;
140✔
1429
        $paramCount      = 0;
140✔
1430
        $hasAttributes   = false;
140✔
1431
        $passByReference = false;
140✔
1432
        $referenceToken  = false;
140✔
1433
        $variableLength  = false;
140✔
1434
        $variadicToken   = false;
140✔
1435
        $typeHint        = '';
140✔
1436
        $typeHintToken   = false;
140✔
1437
        $typeHintEndToken   = false;
140✔
1438
        $nullableType       = false;
140✔
1439
        $visibilityToken    = null;
140✔
1440
        $setVisibilityToken = null;
140✔
1441
        $readonlyToken      = null;
140✔
1442

1443
        for ($i = $paramStart; $i <= $closer; $i++) {
140✔
1444
            // Check to see if this token has a parenthesis or bracket opener. If it does
1445
            // it's likely to be an array which might have arguments in it. This
1446
            // could cause problems in our parsing below, so lets just skip to the
1447
            // end of it.
1448
            if ($this->tokens[$i]['code'] !== T_TYPE_OPEN_PARENTHESIS
140✔
1449
                && isset($this->tokens[$i]['parenthesis_opener']) === true
140✔
1450
            ) {
1451
                // Don't do this if it's the close parenthesis for the method.
1452
                if ($i !== $this->tokens[$i]['parenthesis_closer']) {
140✔
1453
                    $i = $this->tokens[$i]['parenthesis_closer'];
6✔
1454
                    continue;
6✔
1455
                }
1456
            }
1457

1458
            if (isset($this->tokens[$i]['bracket_opener']) === true) {
140✔
1459
                if ($i !== $this->tokens[$i]['bracket_closer']) {
2✔
1460
                    $i = $this->tokens[$i]['bracket_closer'];
2✔
1461
                    continue;
2✔
1462
                }
1463
            }
1464

1465
            switch ($this->tokens[$i]['code']) {
140✔
1466
            case T_ATTRIBUTE:
140✔
1467
                $hasAttributes = true;
4✔
1468

1469
                // Skip to the end of the attribute.
1470
                $i = $this->tokens[$i]['attribute_closer'];
4✔
1471
                break;
4✔
1472
            case T_BITWISE_AND:
140✔
1473
                if ($defaultStart === null) {
36✔
1474
                    $passByReference = true;
34✔
1475
                    $referenceToken  = $i;
34✔
1476
                }
1477
                break;
36✔
1478
            case T_VARIABLE:
140✔
1479
                $currVar = $i;
134✔
1480
                break;
134✔
1481
            case T_ELLIPSIS:
140✔
1482
                $variableLength = true;
34✔
1483
                $variadicToken  = $i;
34✔
1484
                break;
34✔
1485
            case T_CALLABLE:
140✔
1486
                if ($typeHintToken === false) {
8✔
1487
                    $typeHintToken = $i;
6✔
1488
                }
1489

1490
                $typeHint        .= $this->tokens[$i]['content'];
8✔
1491
                $typeHintEndToken = $i;
8✔
1492
                break;
8✔
1493
            case T_SELF:
140✔
1494
            case T_PARENT:
140✔
1495
            case T_STATIC:
140✔
1496
                // Self and parent are valid, static invalid, but was probably intended as type hint.
1497
                if (isset($defaultStart) === false) {
12✔
1498
                    if ($typeHintToken === false) {
10✔
1499
                        $typeHintToken = $i;
8✔
1500
                    }
1501

1502
                    $typeHint        .= $this->tokens[$i]['content'];
10✔
1503
                    $typeHintEndToken = $i;
10✔
1504
                }
1505
                break;
12✔
1506
            case T_STRING:
140✔
1507
            case T_NAME_QUALIFIED:
140✔
1508
            case T_NAME_FULLY_QUALIFIED:
140✔
1509
            case T_NAME_RELATIVE:
140✔
1510
                // This is an identifier name, so it may be a type declaration, but it could
1511
                // also be a constant used as a default value.
1512
                $prevComma = false;
94✔
1513
                for ($t = $i; $t >= $opener; $t--) {
94✔
1514
                    if ($this->tokens[$t]['code'] === T_COMMA) {
94✔
1515
                        $prevComma = $t;
42✔
1516
                        break;
42✔
1517
                    }
1518
                }
1519

1520
                if ($prevComma !== false) {
94✔
1521
                    $nextEquals = false;
42✔
1522
                    for ($t = $prevComma; $t < $i; $t++) {
42✔
1523
                        if ($this->tokens[$t]['code'] === T_EQUAL) {
42✔
1524
                            $nextEquals = $t;
6✔
1525
                            break;
6✔
1526
                        }
1527
                    }
1528

1529
                    if ($nextEquals !== false) {
42✔
1530
                        break;
6✔
1531
                    }
1532
                }
1533

1534
                if ($defaultStart === null) {
92✔
1535
                    if ($typeHintToken === false) {
90✔
1536
                        $typeHintToken = $i;
80✔
1537
                    }
1538

1539
                    $typeHint        .= $this->tokens[$i]['content'];
90✔
1540
                    $typeHintEndToken = $i;
90✔
1541
                }
1542
                break;
92✔
1543
            case T_NAMESPACE:
140✔
1544
            case T_NS_SEPARATOR:
140✔
1545
            case T_TYPE_UNION:
140✔
1546
            case T_TYPE_INTERSECTION:
140✔
1547
            case T_TYPE_OPEN_PARENTHESIS:
140✔
1548
            case T_TYPE_CLOSE_PARENTHESIS:
140✔
1549
            case T_FALSE:
140✔
1550
            case T_TRUE:
140✔
1551
            case T_NULL:
140✔
1552
                // Part of a type hint or default value.
1553
                if ($defaultStart === null) {
66✔
1554
                    if ($typeHintToken === false) {
60✔
1555
                        $typeHintToken = $i;
18✔
1556
                    }
1557

1558
                    $typeHint        .= $this->tokens[$i]['content'];
60✔
1559
                    $typeHintEndToken = $i;
60✔
1560
                }
1561
                break;
66✔
1562
            case T_NULLABLE:
140✔
1563
                if ($defaultStart === null) {
44✔
1564
                    $nullableType     = true;
44✔
1565
                    $typeHint        .= $this->tokens[$i]['content'];
44✔
1566
                    $typeHintEndToken = $i;
44✔
1567
                }
1568
                break;
44✔
1569
            case T_PUBLIC:
140✔
1570
            case T_PROTECTED:
140✔
1571
            case T_PRIVATE:
140✔
1572
                if ($defaultStart === null) {
18✔
1573
                    $visibilityToken = $i;
18✔
1574
                }
1575
                break;
18✔
1576
            case T_PUBLIC_SET:
140✔
1577
            case T_PROTECTED_SET:
140✔
1578
            case T_PRIVATE_SET:
140✔
1579
                if ($defaultStart === null) {
2✔
1580
                    $setVisibilityToken = $i;
2✔
1581
                }
1582
                break;
2✔
1583
            case T_READONLY:
140✔
1584
                if ($defaultStart === null) {
8✔
1585
                    $readonlyToken = $i;
8✔
1586
                }
1587
                break;
8✔
1588
            case T_CLOSE_PARENTHESIS:
140✔
1589
            case T_COMMA:
130✔
1590
                // If it's null, then there must be no parameters for this
1591
                // method.
1592
                if ($currVar === null) {
140✔
1593
                    continue 2;
24✔
1594
                }
1595

1596
                $vars[$paramCount]            = [];
134✔
1597
                $vars[$paramCount]['token']   = $currVar;
134✔
1598
                $vars[$paramCount]['name']    = $this->tokens[$currVar]['content'];
134✔
1599
                $vars[$paramCount]['content'] = trim($this->getTokensAsString($paramStart, ($i - $paramStart)));
134✔
1600

1601
                if ($defaultStart !== null) {
134✔
1602
                    $vars[$paramCount]['default']       = trim($this->getTokensAsString($defaultStart, ($i - $defaultStart)));
44✔
1603
                    $vars[$paramCount]['default_token'] = $defaultStart;
44✔
1604
                    $vars[$paramCount]['default_equal_token'] = $equalToken;
44✔
1605
                }
1606

1607
                $vars[$paramCount]['has_attributes']      = $hasAttributes;
134✔
1608
                $vars[$paramCount]['pass_by_reference']   = $passByReference;
134✔
1609
                $vars[$paramCount]['reference_token']     = $referenceToken;
134✔
1610
                $vars[$paramCount]['variable_length']     = $variableLength;
134✔
1611
                $vars[$paramCount]['variadic_token']      = $variadicToken;
134✔
1612
                $vars[$paramCount]['type_hint']           = $typeHint;
134✔
1613
                $vars[$paramCount]['type_hint_token']     = $typeHintToken;
134✔
1614
                $vars[$paramCount]['type_hint_end_token'] = $typeHintEndToken;
134✔
1615
                $vars[$paramCount]['nullable_type']       = $nullableType;
134✔
1616

1617
                if ($visibilityToken !== null || $setVisibilityToken !== null || $readonlyToken !== null) {
134✔
1618
                    $vars[$paramCount]['property_visibility'] = 'public';
20✔
1619
                    $vars[$paramCount]['visibility_token']    = false;
20✔
1620

1621
                    if ($visibilityToken !== null) {
20✔
1622
                        $vars[$paramCount]['property_visibility'] = $this->tokens[$visibilityToken]['content'];
18✔
1623
                        $vars[$paramCount]['visibility_token']    = $visibilityToken;
18✔
1624
                    }
1625

1626
                    if ($setVisibilityToken !== null) {
20✔
1627
                        $vars[$paramCount]['set_visibility']       = $this->tokens[$setVisibilityToken]['content'];
2✔
1628
                        $vars[$paramCount]['set_visibility_token'] = $setVisibilityToken;
2✔
1629
                    }
1630

1631
                    $vars[$paramCount]['property_readonly'] = false;
20✔
1632
                    if ($readonlyToken !== null) {
20✔
1633
                        $vars[$paramCount]['property_readonly'] = true;
8✔
1634
                        $vars[$paramCount]['readonly_token']    = $readonlyToken;
8✔
1635
                    }
1636
                }
1637

1638
                if ($this->tokens[$i]['code'] === T_COMMA) {
134✔
1639
                    $vars[$paramCount]['comma_token'] = $i;
68✔
1640
                } else {
1641
                    $vars[$paramCount]['comma_token'] = false;
116✔
1642
                }
1643

1644
                // Reset the vars, as we are about to process the next parameter.
1645
                $currVar            = null;
134✔
1646
                $paramStart         = ($i + 1);
134✔
1647
                $defaultStart       = null;
134✔
1648
                $equalToken         = null;
134✔
1649
                $hasAttributes      = false;
134✔
1650
                $passByReference    = false;
134✔
1651
                $referenceToken     = false;
134✔
1652
                $variableLength     = false;
134✔
1653
                $variadicToken      = false;
134✔
1654
                $typeHint           = '';
134✔
1655
                $typeHintToken      = false;
134✔
1656
                $typeHintEndToken   = false;
134✔
1657
                $nullableType       = false;
134✔
1658
                $visibilityToken    = null;
134✔
1659
                $setVisibilityToken = null;
134✔
1660
                $readonlyToken      = null;
134✔
1661

1662
                $paramCount++;
134✔
1663
                break;
134✔
1664
            case T_EQUAL:
130✔
1665
                $defaultStart = $this->findNext(Tokens::EMPTY_TOKENS, ($i + 1), null, true);
44✔
1666
                $equalToken   = $i;
44✔
1667
                break;
44✔
1668
            }//end switch
1669
        }//end for
1670

1671
        return $vars;
140✔
1672

1673
    }//end getMethodParameters()
1674

1675

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

1714
        if ($this->tokens[$stackPtr]['code'] === T_FUNCTION) {
112✔
1715
            $valid = [
82✔
1716
                T_PUBLIC     => T_PUBLIC,
82✔
1717
                T_PRIVATE    => T_PRIVATE,
82✔
1718
                T_PROTECTED  => T_PROTECTED,
82✔
1719
                T_STATIC     => T_STATIC,
82✔
1720
                T_FINAL      => T_FINAL,
82✔
1721
                T_ABSTRACT   => T_ABSTRACT,
82✔
1722
                T_WHITESPACE => T_WHITESPACE,
82✔
1723
                T_COMMENT    => T_COMMENT,
82✔
1724
            ];
82✔
1725
        } else {
1726
            $valid = [
30✔
1727
                T_STATIC     => T_STATIC,
30✔
1728
                T_WHITESPACE => T_WHITESPACE,
30✔
1729
                T_COMMENT    => T_COMMENT,
30✔
1730
            ];
30✔
1731
        }
1732

1733
        $scope          = 'public';
112✔
1734
        $scopeSpecified = false;
112✔
1735
        $isAbstract     = false;
112✔
1736
        $isFinal        = false;
112✔
1737
        $isStatic       = false;
112✔
1738

1739
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
112✔
1740
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
112✔
1741
                break;
110✔
1742
            }
1743

1744
            switch ($this->tokens[$i]['code']) {
110✔
1745
            case T_PUBLIC:
110✔
1746
                $scope          = 'public';
12✔
1747
                $scopeSpecified = true;
12✔
1748
                break;
12✔
1749
            case T_PRIVATE:
110✔
1750
                $scope          = 'private';
6✔
1751
                $scopeSpecified = true;
6✔
1752
                break;
6✔
1753
            case T_PROTECTED:
110✔
1754
                $scope          = 'protected';
6✔
1755
                $scopeSpecified = true;
6✔
1756
                break;
6✔
1757
            case T_ABSTRACT:
110✔
1758
                $isAbstract = true;
6✔
1759
                break;
6✔
1760
            case T_FINAL:
110✔
1761
                $isFinal = true;
2✔
1762
                break;
2✔
1763
            case T_STATIC:
110✔
1764
                $isStatic = true;
4✔
1765
                break;
4✔
1766
            }//end switch
1767
        }//end for
1768

1769
        $returnType         = '';
112✔
1770
        $returnTypeToken    = false;
112✔
1771
        $returnTypeEndToken = false;
112✔
1772
        $nullableReturnType = false;
112✔
1773
        $hasBody            = true;
112✔
1774

1775
        if (isset($this->tokens[$stackPtr]['parenthesis_closer']) === true) {
112✔
1776
            $scopeOpener = null;
112✔
1777
            if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
112✔
1778
                $scopeOpener = $this->tokens[$stackPtr]['scope_opener'];
102✔
1779
            }
1780

1781
            $valid  = Tokens::NAME_TOKENS;
112✔
1782
            $valid += [
112✔
1783
                T_CALLABLE               => T_CALLABLE,
112✔
1784
                T_SELF                   => T_SELF,
112✔
1785
                T_PARENT                 => T_PARENT,
112✔
1786
                T_STATIC                 => T_STATIC,
112✔
1787
                T_FALSE                  => T_FALSE,
112✔
1788
                T_TRUE                   => T_TRUE,
112✔
1789
                T_NULL                   => T_NULL,
112✔
1790
                T_TYPE_UNION             => T_TYPE_UNION,
112✔
1791
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
112✔
1792
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
112✔
1793
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
112✔
1794
            ];
112✔
1795

1796
            for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) {
112✔
1797
                if (($scopeOpener === null && $this->tokens[$i]['code'] === T_SEMICOLON)
112✔
1798
                    || ($scopeOpener !== null && $i === $scopeOpener)
112✔
1799
                ) {
1800
                    // End of function definition.
1801
                    break;
112✔
1802
                }
1803

1804
                // Skip over closure use statements.
1805
                if ($this->tokens[$i]['code'] === T_USE) {
112✔
1806
                    if (isset($this->tokens[$i]['parenthesis_closer']) === false) {
10✔
1807
                        // Live coding/parse error, stop parsing.
1808
                        break;
×
1809
                    }
1810

1811
                    $i = $this->tokens[$i]['parenthesis_closer'];
10✔
1812
                    continue;
10✔
1813
                }
1814

1815
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
112✔
1816
                    $nullableReturnType = true;
26✔
1817
                }
1818

1819
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
112✔
1820
                    if ($returnTypeToken === false) {
96✔
1821
                        $returnTypeToken = $i;
96✔
1822
                    }
1823

1824
                    $returnType        .= $this->tokens[$i]['content'];
96✔
1825
                    $returnTypeEndToken = $i;
96✔
1826
                }
1827
            }//end for
1828

1829
            if ($this->tokens[$stackPtr]['code'] === T_FN) {
112✔
1830
                $bodyToken = T_FN_ARROW;
12✔
1831
            } else {
1832
                $bodyToken = T_OPEN_CURLY_BRACKET;
100✔
1833
            }
1834

1835
            $end     = $this->findNext([$bodyToken, T_SEMICOLON], $this->tokens[$stackPtr]['parenthesis_closer']);
112✔
1836
            $hasBody = $this->tokens[$end]['code'] === $bodyToken;
112✔
1837
        }//end if
1838

1839
        if ($returnType !== '' && $nullableReturnType === true) {
112✔
1840
            $returnType = '?'.$returnType;
26✔
1841
        }
1842

1843
        return [
112✔
1844
            'scope'                 => $scope,
112✔
1845
            'scope_specified'       => $scopeSpecified,
112✔
1846
            'return_type'           => $returnType,
112✔
1847
            'return_type_token'     => $returnTypeToken,
112✔
1848
            'return_type_end_token' => $returnTypeEndToken,
112✔
1849
            'nullable_return_type'  => $nullableReturnType,
112✔
1850
            'is_abstract'           => $isAbstract,
112✔
1851
            'is_final'              => $isFinal,
112✔
1852
            'is_static'             => $isStatic,
112✔
1853
            'has_body'              => $hasBody,
112✔
1854
        ];
112✔
1855

1856
    }//end getMethodProperties()
1857

1858

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

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

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

1921
        $valid = [
222✔
1922
            T_STATIC   => T_STATIC,
222✔
1923
            T_VAR      => T_VAR,
222✔
1924
            T_READONLY => T_READONLY,
222✔
1925
            T_FINAL    => T_FINAL,
222✔
1926
            T_ABSTRACT => T_ABSTRACT,
222✔
1927
        ];
222✔
1928

1929
        $valid += Tokens::SCOPE_MODIFIERS;
222✔
1930
        $valid += Tokens::EMPTY_TOKENS;
222✔
1931

1932
        $scope          = 'public';
222✔
1933
        $scopeSpecified = false;
222✔
1934
        $setScope       = false;
222✔
1935
        $isStatic       = false;
222✔
1936
        $isReadonly     = false;
222✔
1937
        $isFinal        = false;
222✔
1938
        $isAbstract     = false;
222✔
1939

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

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

1955
            switch ($this->tokens[$i]['code']) {
222✔
1956
            case T_PUBLIC:
222✔
1957
                $scope          = 'public';
108✔
1958
                $scopeSpecified = true;
108✔
1959
                break;
108✔
1960
            case T_PRIVATE:
222✔
1961
                $scope          = 'private';
40✔
1962
                $scopeSpecified = true;
40✔
1963
                break;
40✔
1964
            case T_PROTECTED:
222✔
1965
                $scope          = 'protected';
36✔
1966
                $scopeSpecified = true;
36✔
1967
                break;
36✔
1968
            case T_PUBLIC_SET:
222✔
1969
                $setScope = 'public';
6✔
1970
                break;
6✔
1971
            case T_PROTECTED_SET:
222✔
1972
                $setScope = 'protected';
8✔
1973
                break;
8✔
1974
            case T_PRIVATE_SET:
222✔
1975
                $setScope = 'private';
6✔
1976
                break;
6✔
1977
            case T_STATIC:
222✔
1978
                $isStatic = true;
48✔
1979
                break;
48✔
1980
            case T_READONLY:
222✔
1981
                $isReadonly = true;
28✔
1982
                break;
28✔
1983
            case T_FINAL:
222✔
1984
                $isFinal = true;
20✔
1985
                break;
20✔
1986
            case T_ABSTRACT:
222✔
1987
                $isAbstract = true;
20✔
1988
                break;
20✔
1989
            }//end switch
1990
        }//end for
1991

1992
        $type         = '';
222✔
1993
        $typeToken    = false;
222✔
1994
        $typeEndToken = false;
222✔
1995
        $nullableType = false;
222✔
1996

1997
        if ($i < $stackPtr) {
222✔
1998
            // We've found a type.
1999
            $valid  = Tokens::NAME_TOKENS;
174✔
2000
            $valid += [
174✔
2001
                T_CALLABLE               => T_CALLABLE,
174✔
2002
                T_SELF                   => T_SELF,
174✔
2003
                T_PARENT                 => T_PARENT,
174✔
2004
                T_FALSE                  => T_FALSE,
174✔
2005
                T_TRUE                   => T_TRUE,
174✔
2006
                T_NULL                   => T_NULL,
174✔
2007
                T_TYPE_UNION             => T_TYPE_UNION,
174✔
2008
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
174✔
2009
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
174✔
2010
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
174✔
2011
            ];
174✔
2012

2013
            for ($i; $i < $stackPtr; $i++) {
174✔
2014
                if ($this->tokens[$i]['code'] === T_VARIABLE) {
174✔
2015
                    // Hit another variable in a group definition.
2016
                    break;
20✔
2017
                }
2018

2019
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
158✔
2020
                    $nullableType = true;
38✔
2021
                }
2022

2023
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
158✔
2024
                    $typeEndToken = $i;
158✔
2025
                    if ($typeToken === false) {
158✔
2026
                        $typeToken = $i;
158✔
2027
                    }
2028

2029
                    $type .= $this->tokens[$i]['content'];
158✔
2030
                }
2031
            }
2032

2033
            if ($type !== '' && $nullableType === true) {
174✔
2034
                $type = '?'.$type;
38✔
2035
            }
2036
        }//end if
2037

2038
        return [
222✔
2039
            'scope'           => $scope,
222✔
2040
            'scope_specified' => $scopeSpecified,
222✔
2041
            'set_scope'       => $setScope,
222✔
2042
            'is_static'       => $isStatic,
222✔
2043
            'is_readonly'     => $isReadonly,
222✔
2044
            'is_final'        => $isFinal,
222✔
2045
            'is_abstract'     => $isAbstract,
222✔
2046
            'type'            => $type,
222✔
2047
            'type_token'      => $typeToken,
222✔
2048
            'type_end_token'  => $typeEndToken,
222✔
2049
            'nullable_type'   => $nullableType,
222✔
2050
        ];
222✔
2051

2052
    }//end getMemberProperties()
2053

2054

2055
    /**
2056
     * Returns the visibility and implementation properties of a class.
2057
     *
2058
     * The format of the return value is:
2059
     * <code>
2060
     *   array(
2061
     *    'is_abstract' => boolean, // TRUE if the abstract keyword was found.
2062
     *    'is_final'    => boolean, // TRUE if the final keyword was found.
2063
     *    'is_readonly' => boolean, // TRUE if the readonly keyword was found.
2064
     *   );
2065
     * </code>
2066
     *
2067
     * @param int $stackPtr The position in the stack of the T_CLASS token to
2068
     *                      acquire the properties for.
2069
     *
2070
     * @return array
2071
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
2072
     *                                                      T_CLASS token.
2073
     */
2074
    public function getClassProperties($stackPtr)
28✔
2075
    {
2076
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS) {
28✔
2077
            throw new RuntimeException('$stackPtr must be of type T_CLASS');
6✔
2078
        }
2079

2080
        $valid = [
22✔
2081
            T_FINAL      => T_FINAL,
22✔
2082
            T_ABSTRACT   => T_ABSTRACT,
22✔
2083
            T_READONLY   => T_READONLY,
22✔
2084
            T_WHITESPACE => T_WHITESPACE,
22✔
2085
            T_COMMENT    => T_COMMENT,
22✔
2086
        ];
22✔
2087

2088
        $isAbstract = false;
22✔
2089
        $isFinal    = false;
22✔
2090
        $isReadonly = false;
22✔
2091

2092
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
22✔
2093
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
22✔
2094
                break;
22✔
2095
            }
2096

2097
            switch ($this->tokens[$i]['code']) {
22✔
2098
            case T_ABSTRACT:
22✔
2099
                $isAbstract = true;
10✔
2100
                break;
10✔
2101

2102
            case T_FINAL:
22✔
2103
                $isFinal = true;
8✔
2104
                break;
8✔
2105

2106
            case T_READONLY:
22✔
2107
                $isReadonly = true;
10✔
2108
                break;
10✔
2109
            }
2110
        }//end for
2111

2112
        return [
22✔
2113
            'is_abstract' => $isAbstract,
22✔
2114
            'is_final'    => $isFinal,
22✔
2115
            'is_readonly' => $isReadonly,
22✔
2116
        ];
22✔
2117

2118
    }//end getClassProperties()
2119

2120

2121
    /**
2122
     * Determine if the passed token is a reference operator.
2123
     *
2124
     * Returns true if the specified token position represents a reference.
2125
     * Returns false if the token represents a bitwise operator.
2126
     *
2127
     * @param int $stackPtr The position of the T_BITWISE_AND token.
2128
     *
2129
     * @return boolean
2130
     */
2131
    public function isReference($stackPtr)
152✔
2132
    {
2133
        if ($this->tokens[$stackPtr]['code'] !== T_BITWISE_AND) {
152✔
2134
            return false;
6✔
2135
        }
2136

2137
        $tokenBefore = $this->findPrevious(
146✔
2138
            Tokens::EMPTY_TOKENS,
146✔
2139
            ($stackPtr - 1),
146✔
2140
            null,
146✔
2141
            true
146✔
2142
        );
146✔
2143

2144
        if ($this->tokens[$tokenBefore]['code'] === T_FUNCTION
146✔
2145
            || $this->tokens[$tokenBefore]['code'] === T_CLOSURE
144✔
2146
            || $this->tokens[$tokenBefore]['code'] === T_FN
146✔
2147
        ) {
2148
            // Function returns a reference.
2149
            return true;
6✔
2150
        }
2151

2152
        if ($this->tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
140✔
2153
            // Inside a foreach loop or array assignment, this is a reference.
2154
            return true;
12✔
2155
        }
2156

2157
        if ($this->tokens[$tokenBefore]['code'] === T_AS) {
128✔
2158
            // Inside a foreach loop, this is a reference.
2159
            return true;
2✔
2160
        }
2161

2162
        if (isset(Tokens::ASSIGNMENT_TOKENS[$this->tokens[$tokenBefore]['code']]) === true) {
126✔
2163
            // This is directly after an assignment. It's a reference. Even if
2164
            // it is part of an operation, the other tests will handle it.
2165
            return true;
14✔
2166
        }
2167

2168
        $tokenAfter = $this->findNext(
112✔
2169
            Tokens::EMPTY_TOKENS,
112✔
2170
            ($stackPtr + 1),
112✔
2171
            null,
112✔
2172
            true
112✔
2173
        );
112✔
2174

2175
        if ($this->tokens[$tokenAfter]['code'] === T_NEW) {
112✔
2176
            return true;
2✔
2177
        }
2178

2179
        if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === true) {
110✔
2180
            $brackets    = $this->tokens[$stackPtr]['nested_parenthesis'];
78✔
2181
            $lastBracket = array_pop($brackets);
78✔
2182
            if (isset($this->tokens[$lastBracket]['parenthesis_owner']) === true) {
78✔
2183
                $owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']];
54✔
2184
                if ($owner['code'] === T_FUNCTION
54✔
2185
                    || $owner['code'] === T_CLOSURE
46✔
2186
                    || $owner['code'] === T_FN
28✔
2187
                    || $owner['code'] === T_USE
54✔
2188
                ) {
2189
                    $params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']);
40✔
2190
                    foreach ($params as $param) {
40✔
2191
                        if ($param['reference_token'] === $stackPtr) {
40✔
2192
                            // Function parameter declared to be passed by reference.
2193
                            return true;
30✔
2194
                        }
2195
                    }
2196
                }//end if
2197
            }//end if
2198
        }//end if
2199

2200
        // Pass by reference in function calls and assign by reference in arrays.
2201
        if ($this->tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
80✔
2202
            || $this->tokens[$tokenBefore]['code'] === T_COMMA
72✔
2203
            || $this->tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY
80✔
2204
        ) {
2205
            if ($this->tokens[$tokenAfter]['code'] === T_VARIABLE) {
56✔
2206
                return true;
40✔
2207
            } else {
2208
                $skip   = Tokens::EMPTY_TOKENS;
16✔
2209
                $skip  += Tokens::NAME_TOKENS;
16✔
2210
                $skip[] = T_SELF;
16✔
2211
                $skip[] = T_PARENT;
16✔
2212
                $skip[] = T_STATIC;
16✔
2213
                $skip[] = T_DOUBLE_COLON;
16✔
2214

2215
                $nextSignificantAfter = $this->findNext(
16✔
2216
                    $skip,
16✔
2217
                    ($stackPtr + 1),
16✔
2218
                    null,
16✔
2219
                    true
16✔
2220
                );
16✔
2221
                if ($this->tokens[$nextSignificantAfter]['code'] === T_VARIABLE) {
16✔
2222
                    return true;
16✔
2223
                }
2224
            }//end if
2225
        }//end if
2226

2227
        return false;
24✔
2228

2229
    }//end isReference()
2230

2231

2232
    /**
2233
     * Returns the content of the tokens from the specified start position in
2234
     * the token stack for the specified length.
2235
     *
2236
     * @param int  $start       The position to start from in the token stack.
2237
     * @param int  $length      The length of tokens to traverse from the start pos.
2238
     * @param bool $origContent Whether the original content or the tab replaced
2239
     *                          content should be used.
2240
     *
2241
     * @return string The token contents.
2242
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position does not exist.
2243
     */
2244
    public function getTokensAsString($start, $length, $origContent=false)
56✔
2245
    {
2246
        if (is_int($start) === false || isset($this->tokens[$start]) === false) {
56✔
2247
            throw new RuntimeException('The $start position for getTokensAsString() must exist in the token stack');
4✔
2248
        }
2249

2250
        if (is_int($length) === false || $length <= 0) {
52✔
2251
            return '';
6✔
2252
        }
2253

2254
        $str = '';
46✔
2255
        $end = ($start + $length);
46✔
2256
        if ($end > $this->numTokens) {
46✔
2257
            $end = $this->numTokens;
2✔
2258
        }
2259

2260
        for ($i = $start; $i < $end; $i++) {
46✔
2261
            // If tabs are being converted to spaces by the tokeniser, the
2262
            // original content should be used instead of the converted content.
2263
            if ($origContent === true && isset($this->tokens[$i]['orig_content']) === true) {
46✔
2264
                $str .= $this->tokens[$i]['orig_content'];
4✔
2265
            } else {
2266
                $str .= $this->tokens[$i]['content'];
46✔
2267
            }
2268
        }
2269

2270
        return $str;
46✔
2271

2272
    }//end getTokensAsString()
2273

2274

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

2311
        if ($end === null) {
×
2312
            $end = 0;
×
2313
        }
2314

2315
        for ($i = $start; $i >= $end; $i--) {
×
2316
            $found = (bool) $exclude;
×
2317
            foreach ($types as $type) {
×
2318
                if ($this->tokens[$i]['code'] === $type) {
×
2319
                    $found = !$exclude;
×
2320
                    break;
×
2321
                }
2322
            }
2323

2324
            if ($found === true) {
×
2325
                if ($value === null) {
×
2326
                    return $i;
×
2327
                } else if ($this->tokens[$i]['content'] === $value) {
×
2328
                    return $i;
×
2329
                }
2330
            }
2331

2332
            if ($local === true) {
×
2333
                if (isset($this->tokens[$i]['scope_opener']) === true
×
2334
                    && $i === $this->tokens[$i]['scope_closer']
×
2335
                ) {
2336
                    $i = $this->tokens[$i]['scope_opener'];
×
2337
                } else if (isset($this->tokens[$i]['bracket_opener']) === true
×
2338
                    && $i === $this->tokens[$i]['bracket_closer']
×
2339
                ) {
2340
                    $i = $this->tokens[$i]['bracket_opener'];
×
2341
                } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
×
2342
                    && $i === $this->tokens[$i]['parenthesis_closer']
×
2343
                ) {
2344
                    $i = $this->tokens[$i]['parenthesis_opener'];
×
2345
                } else if ($this->tokens[$i]['code'] === T_SEMICOLON) {
×
2346
                    break;
×
2347
                }
2348
            }
2349
        }//end for
2350

2351
        return false;
×
2352

2353
    }//end findPrevious()
2354

2355

2356
    /**
2357
     * Returns the position of the next specified token(s).
2358
     *
2359
     * If a value is specified, the next token of the specified type(s)
2360
     * containing the specified value will be returned.
2361
     *
2362
     * Returns false if no token can be found.
2363
     *
2364
     * @param int|string|array $types   The type(s) of tokens to search for.
2365
     * @param int              $start   The position to start searching from in the
2366
     *                                  token stack.
2367
     * @param int|null         $end     The end position to fail if no token is found.
2368
     *                                  if not specified or null, end will default to
2369
     *                                  the end of the token stack.
2370
     * @param bool             $exclude If true, find the next token that is NOT of
2371
     *                                  a type specified in $types.
2372
     * @param string|null      $value   The value that the token(s) must be equal to.
2373
     *                                  If value is omitted, tokens with any value will
2374
     *                                  be returned.
2375
     * @param bool             $local   If true, tokens outside the current statement
2376
     *                                  will not be checked. i.e., checking will stop
2377
     *                                  at the next semicolon found.
2378
     *
2379
     * @return int|false
2380
     * @see    findPrevious()
2381
     */
2382
    public function findNext(
×
2383
        $types,
2384
        $start,
2385
        $end=null,
2386
        $exclude=false,
2387
        $value=null,
2388
        $local=false
2389
    ) {
2390
        $types = (array) $types;
×
2391

2392
        if ($end === null || $end > $this->numTokens) {
×
2393
            $end = $this->numTokens;
×
2394
        }
2395

2396
        for ($i = $start; $i < $end; $i++) {
×
2397
            $found = (bool) $exclude;
×
2398
            foreach ($types as $type) {
×
2399
                if ($this->tokens[$i]['code'] === $type) {
×
2400
                    $found = !$exclude;
×
2401
                    break;
×
2402
                }
2403
            }
2404

2405
            if ($found === true) {
×
2406
                if ($value === null) {
×
2407
                    return $i;
×
2408
                } else if ($this->tokens[$i]['content'] === $value) {
×
2409
                    return $i;
×
2410
                }
2411
            }
2412

2413
            if ($local === true && $this->tokens[$i]['code'] === T_SEMICOLON) {
×
2414
                break;
×
2415
            }
2416
        }//end for
2417

2418
        return false;
×
2419

2420
    }//end findNext()
2421

2422

2423
    /**
2424
     * Returns the position of the first non-whitespace token in a statement.
2425
     *
2426
     * @param int              $start  The position to start searching from in the token stack.
2427
     * @param int|string|array $ignore Token types that should not be considered stop points.
2428
     *
2429
     * @return int
2430
     */
2431
    public function findStartOfStatement($start, $ignore=null)
140✔
2432
    {
2433
        $startTokens = Tokens::BLOCK_OPENERS;
140✔
2434
        $startTokens[T_OPEN_SHORT_ARRAY]   = true;
140✔
2435
        $startTokens[T_OPEN_TAG]           = true;
140✔
2436
        $startTokens[T_OPEN_TAG_WITH_ECHO] = true;
140✔
2437

2438
        $endTokens = [
140✔
2439
            T_CLOSE_TAG    => true,
140✔
2440
            T_COLON        => true,
140✔
2441
            T_COMMA        => true,
140✔
2442
            T_DOUBLE_ARROW => true,
140✔
2443
            T_MATCH_ARROW  => true,
140✔
2444
            T_SEMICOLON    => true,
140✔
2445
        ];
140✔
2446

2447
        if ($ignore !== null) {
140✔
2448
            $ignore = (array) $ignore;
×
2449
            foreach ($ignore as $code) {
×
2450
                if (isset($startTokens[$code]) === true) {
×
2451
                    unset($startTokens[$code]);
×
2452
                }
2453

2454
                if (isset($endTokens[$code]) === true) {
×
2455
                    unset($endTokens[$code]);
×
2456
                }
2457
            }
2458
        }
2459

2460
        // If the start token is inside the case part of a match expression,
2461
        // find the start of the condition. If it's in the statement part, find
2462
        // the token that comes after the match arrow.
2463
        if (empty($this->tokens[$start]['conditions']) === false) {
140✔
2464
            $conditions         = $this->tokens[$start]['conditions'];
108✔
2465
            $lastConditionOwner = end($conditions);
108✔
2466
            $matchExpression    = key($conditions);
108✔
2467

2468
            if ($lastConditionOwner === T_MATCH
108✔
2469
                // Check if the $start token is at the same parentheses nesting level as the match token.
2470
                && ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === true
108✔
2471
                && empty($this->tokens[$start]['nested_parenthesis']) === true)
108✔
2472
                || ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === false
108✔
2473
                && empty($this->tokens[$start]['nested_parenthesis']) === false)
108✔
2474
                && $this->tokens[$matchExpression]['nested_parenthesis'] === $this->tokens[$start]['nested_parenthesis']))
108✔
2475
            ) {
2476
                // Walk back to the previous match arrow (if it exists).
2477
                $lastComma          = null;
30✔
2478
                $inNestedExpression = false;
30✔
2479
                for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
30✔
2480
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_MATCH_ARROW) {
30✔
2481
                        break;
22✔
2482
                    }
2483

2484
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_COMMA) {
30✔
2485
                        $lastComma = $prevMatch;
16✔
2486
                        continue;
16✔
2487
                    }
2488

2489
                    // Skip nested statements.
2490
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
30✔
2491
                        && $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
30✔
2492
                    ) {
2493
                        $prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
8✔
2494
                        continue;
8✔
2495
                    }
2496

2497
                    if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
30✔
2498
                        && $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
30✔
2499
                    ) {
2500
                        $prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
10✔
2501
                        continue;
10✔
2502
                    }
2503

2504
                    // Stop if we're _within_ a nested short array statement, which may contain comma's too.
2505
                    // No need to deal with parentheses, those are handled above via the `nested_parenthesis` checks.
2506
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
30✔
2507
                        && $this->tokens[$prevMatch]['bracket_closer'] > $start
30✔
2508
                    ) {
2509
                        $inNestedExpression = true;
10✔
2510
                        break;
10✔
2511
                    }
2512
                }//end for
2513

2514
                if ($inNestedExpression === false) {
30✔
2515
                    // $prevMatch will now either be the scope opener or a match arrow.
2516
                    // If it is the scope opener, go the first non-empty token after. $start will have been part of the first condition.
2517
                    if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) {
22✔
2518
                        // We're before the arrow in the first case.
2519
                        $next = $this->findNext(Tokens::EMPTY_TOKENS, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
8✔
2520
                        if ($next === false) {
8✔
2521
                            // Shouldn't be possible.
2522
                            return $start;
×
2523
                        }
2524

2525
                        return $next;
8✔
2526
                    }
2527

2528
                    // Okay, so we found a match arrow.
2529
                    // If $start was part of the "next" condition, the last comma will be set.
2530
                    // Otherwise, $start must have been part of a return expression.
2531
                    if (isset($lastComma) === true && $lastComma > $prevMatch) {
22✔
2532
                        $prevMatch = $lastComma;
10✔
2533
                    }
2534

2535
                    // In both cases, go to the first non-empty token after.
2536
                    $next = $this->findNext(Tokens::EMPTY_TOKENS, ($prevMatch + 1), null, true);
22✔
2537
                    if ($next === false) {
22✔
2538
                        // Shouldn't be possible.
2539
                        return $start;
×
2540
                    }
2541

2542
                    return $next;
22✔
2543
                }//end if
2544
            }//end if
2545
        }//end if
2546

2547
        $lastNotEmpty = $start;
120✔
2548

2549
        // If we are starting at a token that ends a scope block, skip to
2550
        // the start and continue from there.
2551
        // If we are starting at a token that ends a statement, skip this
2552
        // token so we find the true start of the statement.
2553
        while (isset($endTokens[$this->tokens[$start]['code']]) === true
120✔
2554
            || (isset($this->tokens[$start]['scope_condition']) === true
120✔
2555
            && $start === $this->tokens[$start]['scope_closer'])
120✔
2556
        ) {
2557
            if (isset($this->tokens[$start]['scope_condition']) === true) {
34✔
2558
                $start = $this->tokens[$start]['scope_condition'];
18✔
2559
            } else {
2560
                $start--;
20✔
2561
            }
2562
        }
2563

2564
        for ($i = $start; $i >= 0; $i--) {
120✔
2565
            if (isset($startTokens[$this->tokens[$i]['code']]) === true
120✔
2566
                || isset($endTokens[$this->tokens[$i]['code']]) === true
120✔
2567
            ) {
2568
                // Found the end of the previous statement.
2569
                return $lastNotEmpty;
120✔
2570
            }
2571

2572
            if (isset($this->tokens[$i]['scope_opener']) === true
118✔
2573
                && $i === $this->tokens[$i]['scope_closer']
118✔
2574
                && $this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS
118✔
2575
                && $this->tokens[$i]['code'] !== T_END_NOWDOC
118✔
2576
                && $this->tokens[$i]['code'] !== T_END_HEREDOC
118✔
2577
                && $this->tokens[$i]['code'] !== T_BREAK
118✔
2578
                && $this->tokens[$i]['code'] !== T_RETURN
118✔
2579
                && $this->tokens[$i]['code'] !== T_CONTINUE
118✔
2580
                && $this->tokens[$i]['code'] !== T_THROW
118✔
2581
                && $this->tokens[$i]['code'] !== T_EXIT
118✔
2582
                && $this->tokens[$i]['code'] !== T_GOTO
118✔
2583
            ) {
2584
                // Found the end of the previous scope block.
2585
                return $lastNotEmpty;
2✔
2586
            }
2587

2588
            // Skip nested statements.
2589
            if (isset($this->tokens[$i]['bracket_opener']) === true
118✔
2590
                && $i === $this->tokens[$i]['bracket_closer']
118✔
2591
            ) {
2592
                $i = $this->tokens[$i]['bracket_opener'];
2✔
2593
            } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
118✔
2594
                && $i === $this->tokens[$i]['parenthesis_closer']
118✔
2595
            ) {
2596
                $i = $this->tokens[$i]['parenthesis_opener'];
14✔
2597
            } else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
118✔
2598
                $start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
4✔
2599
                if ($start !== false) {
4✔
2600
                    $i = $start;
4✔
2601
                }
2602
            }//end if
2603

2604
            if (isset(Tokens::EMPTY_TOKENS[$this->tokens[$i]['code']]) === false) {
118✔
2605
                $lastNotEmpty = $i;
118✔
2606
            }
2607
        }//end for
2608

2609
        return 0;
×
2610

2611
    }//end findStartOfStatement()
2612

2613

2614
    /**
2615
     * Returns the position of the last non-whitespace token in a statement.
2616
     *
2617
     * @param int              $start  The position to start searching from in the token stack.
2618
     * @param int|string|array $ignore Token types that should not be considered stop points.
2619
     *
2620
     * @return int
2621
     */
2622
    public function findEndOfStatement($start, $ignore=null)
44✔
2623
    {
2624
        $endTokens = [
44✔
2625
            T_COLON                => true,
44✔
2626
            T_COMMA                => true,
44✔
2627
            T_DOUBLE_ARROW         => true,
44✔
2628
            T_SEMICOLON            => true,
44✔
2629
            T_CLOSE_PARENTHESIS    => true,
44✔
2630
            T_CLOSE_SQUARE_BRACKET => true,
44✔
2631
            T_CLOSE_CURLY_BRACKET  => true,
44✔
2632
            T_CLOSE_SHORT_ARRAY    => true,
44✔
2633
            T_OPEN_TAG             => true,
44✔
2634
            T_CLOSE_TAG            => true,
44✔
2635
        ];
44✔
2636

2637
        if ($ignore !== null) {
44✔
2638
            $ignore = (array) $ignore;
×
2639
            foreach ($ignore as $code) {
×
2640
                unset($endTokens[$code]);
×
2641
            }
2642
        }
2643

2644
        // If the start token is inside the case part of a match expression,
2645
        // advance to the match arrow and continue looking for the
2646
        // end of the statement from there so that we skip over commas.
2647
        if ($this->tokens[$start]['code'] !== T_MATCH_ARROW) {
44✔
2648
            $matchExpression = $this->getCondition($start, T_MATCH);
44✔
2649
            if ($matchExpression !== false) {
44✔
2650
                $beforeArrow    = true;
20✔
2651
                $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']);
20✔
2652
                if ($prevMatchArrow !== false) {
20✔
2653
                    $prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start);
18✔
2654
                    if ($prevComma === false) {
18✔
2655
                        // No comma between this token and the last match arrow,
2656
                        // so this token exists after the arrow and we can continue
2657
                        // checking as normal.
2658
                        $beforeArrow = false;
8✔
2659
                    }
2660
                }
2661

2662
                if ($beforeArrow === true) {
20✔
2663
                    $nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
20✔
2664
                    if ($nextMatchArrow !== false) {
20✔
2665
                        $start = $nextMatchArrow;
20✔
2666
                    }
2667
                }
2668
            }//end if
2669
        }//end if
2670

2671
        $lastNotEmpty = $start;
44✔
2672
        for ($i = $start; $i < $this->numTokens; $i++) {
44✔
2673
            if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
44✔
2674
                // Found the end of the statement.
2675
                if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS
40✔
2676
                    || $this->tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
38✔
2677
                    || $this->tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
38✔
2678
                    || $this->tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
34✔
2679
                    || $this->tokens[$i]['code'] === T_OPEN_TAG
32✔
2680
                    || $this->tokens[$i]['code'] === T_CLOSE_TAG
40✔
2681
                ) {
2682
                    return $lastNotEmpty;
16✔
2683
                }
2684

2685
                return $i;
32✔
2686
            }
2687

2688
            // Skip nested statements.
2689
            if (isset($this->tokens[$i]['scope_closer']) === true
44✔
2690
                && ($i === $this->tokens[$i]['scope_opener']
44✔
2691
                || $i === $this->tokens[$i]['scope_condition'])
44✔
2692
            ) {
2693
                if ($this->tokens[$i]['code'] === T_FN) {
24✔
2694
                    $lastNotEmpty = $this->tokens[$i]['scope_closer'];
12✔
2695
                    $i            = ($this->tokens[$i]['scope_closer'] - 1);
12✔
2696
                    continue;
12✔
2697
                }
2698

2699
                if ($i === $start && isset(Tokens::SCOPE_OPENERS[$this->tokens[$i]['code']]) === true) {
14✔
2700
                    return $this->tokens[$i]['scope_closer'];
6✔
2701
                }
2702

2703
                $i = $this->tokens[$i]['scope_closer'];
10✔
2704
            } else if (isset($this->tokens[$i]['bracket_closer']) === true
32✔
2705
                && $i === $this->tokens[$i]['bracket_opener']
32✔
2706
            ) {
2707
                $i = $this->tokens[$i]['bracket_closer'];
4✔
2708
            } else if (isset($this->tokens[$i]['parenthesis_closer']) === true
32✔
2709
                && $i === $this->tokens[$i]['parenthesis_opener']
32✔
2710
            ) {
2711
                $i = $this->tokens[$i]['parenthesis_closer'];
6✔
2712
            } else if ($this->tokens[$i]['code'] === T_OPEN_USE_GROUP) {
32✔
2713
                $end = $this->findNext(T_CLOSE_USE_GROUP, ($i + 1));
4✔
2714
                if ($end !== false) {
4✔
2715
                    $i = $end;
4✔
2716
                }
2717
            }//end if
2718

2719
            if (isset(Tokens::EMPTY_TOKENS[$this->tokens[$i]['code']]) === false) {
32✔
2720
                $lastNotEmpty = $i;
32✔
2721
            }
2722
        }//end for
2723

2724
        return ($this->numTokens - 1);
2✔
2725

2726
    }//end findEndOfStatement()
2727

2728

2729
    /**
2730
     * Returns the position of the first token on a line, matching given type.
2731
     *
2732
     * Returns false if no token can be found.
2733
     *
2734
     * @param int|string|array $types   The type(s) of tokens to search for.
2735
     * @param int              $start   The position to start searching from in the
2736
     *                                  token stack.
2737
     * @param bool             $exclude If true, find the token that is NOT of
2738
     *                                  the types specified in $types.
2739
     * @param string           $value   The value that the token must be equal to.
2740
     *                                  If value is omitted, tokens with any value will
2741
     *                                  be returned.
2742
     *
2743
     * @return int|false The first token which matches on the line containing the start
2744
     *                   token, between the start of the line and the start token.
2745
     *                   Note: The first token matching might be the start token.
2746
     *                   FALSE when no matching token could be found between the start of
2747
     *                   the line and the start token.
2748
     */
2749
    public function findFirstOnLine($types, $start, $exclude=false, $value=null)
×
2750
    {
2751
        if (is_array($types) === false) {
×
2752
            $types = [$types];
×
2753
        }
2754

2755
        $foundToken = false;
×
2756

2757
        for ($i = $start; $i >= 0; $i--) {
×
2758
            if ($this->tokens[$i]['line'] < $this->tokens[$start]['line']) {
×
2759
                break;
×
2760
            }
2761

2762
            $found = $exclude;
×
2763
            foreach ($types as $type) {
×
2764
                if ($exclude === false) {
×
2765
                    if ($this->tokens[$i]['code'] === $type) {
×
2766
                        $found = true;
×
2767
                        break;
×
2768
                    }
2769
                } else {
2770
                    if ($this->tokens[$i]['code'] === $type) {
×
2771
                        $found = false;
×
2772
                        break;
×
2773
                    }
2774
                }
2775
            }
2776

2777
            if ($found === true) {
×
2778
                if ($value === null) {
×
2779
                    $foundToken = $i;
×
2780
                } else if ($this->tokens[$i]['content'] === $value) {
×
2781
                    $foundToken = $i;
×
2782
                }
2783
            }
2784
        }//end for
2785

2786
        return $foundToken;
×
2787

2788
    }//end findFirstOnLine()
2789

2790

2791
    /**
2792
     * Determine if the passed token has a condition of one of the passed types.
2793
     *
2794
     * @param int              $stackPtr The position of the token we are checking.
2795
     * @param int|string|array $types    The type(s) of tokens to search for.
2796
     *
2797
     * @return boolean
2798
     */
2799
    public function hasCondition($stackPtr, $types)
14✔
2800
    {
2801
        // Check for the existence of the token.
2802
        if (isset($this->tokens[$stackPtr]) === false) {
14✔
2803
            return false;
2✔
2804
        }
2805

2806
        // Make sure the token has conditions.
2807
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
12✔
2808
            return false;
×
2809
        }
2810

2811
        $types      = (array) $types;
12✔
2812
        $conditions = $this->tokens[$stackPtr]['conditions'];
12✔
2813

2814
        foreach ($types as $type) {
12✔
2815
            if (in_array($type, $conditions, true) === true) {
12✔
2816
                // We found a token with the required type.
2817
                return true;
10✔
2818
            }
2819
        }
2820

2821
        return false;
12✔
2822

2823
    }//end hasCondition()
2824

2825

2826
    /**
2827
     * Return the position of the condition for the passed token.
2828
     *
2829
     * Returns FALSE if the token does not have the condition.
2830
     *
2831
     * @param int        $stackPtr The position of the token we are checking.
2832
     * @param int|string $type     The type of token to search for.
2833
     * @param bool       $first    If TRUE, will return the matched condition
2834
     *                             furthest away from the passed token.
2835
     *                             If FALSE, will return the matched condition
2836
     *                             closest to the passed token.
2837
     *
2838
     * @return int|false
2839
     */
2840
    public function getCondition($stackPtr, $type, $first=true)
20✔
2841
    {
2842
        // Check for the existence of the token.
2843
        if (isset($this->tokens[$stackPtr]) === false) {
20✔
2844
            return false;
2✔
2845
        }
2846

2847
        // Make sure the token has conditions.
2848
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
18✔
2849
            return false;
×
2850
        }
2851

2852
        $conditions = $this->tokens[$stackPtr]['conditions'];
18✔
2853
        if ($first === false) {
18✔
2854
            $conditions = array_reverse($conditions, true);
8✔
2855
        }
2856

2857
        foreach ($conditions as $token => $condition) {
18✔
2858
            if ($condition === $type) {
18✔
2859
                return $token;
16✔
2860
            }
2861
        }
2862

2863
        return false;
18✔
2864

2865
    }//end getCondition()
2866

2867

2868
    /**
2869
     * Returns the name of the class that the specified class extends.
2870
     * (works for classes, anonymous classes and interfaces)
2871
     *
2872
     * Returns FALSE on error or if there is no extended class name.
2873
     *
2874
     * @param int $stackPtr The stack position of the class.
2875
     *
2876
     * @return string|false
2877
     */
2878
    public function findExtendedClassName($stackPtr)
40✔
2879
    {
2880
        // Check for the existence of the token.
2881
        if (isset($this->tokens[$stackPtr]) === false) {
40✔
2882
            return false;
2✔
2883
        }
2884

2885
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
38✔
2886
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
38✔
2887
            && $this->tokens[$stackPtr]['code'] !== T_INTERFACE
38✔
2888
        ) {
2889
            return false;
2✔
2890
        }
2891

2892
        if (isset($this->tokens[$stackPtr]['scope_opener']) === false) {
36✔
2893
            return false;
2✔
2894
        }
2895

2896
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
34✔
2897
        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex);
34✔
2898
        if ($extendsIndex === false) {
34✔
2899
            return false;
6✔
2900
        }
2901

2902
        $find   = Tokens::NAME_TOKENS;
28✔
2903
        $find[] = T_WHITESPACE;
28✔
2904

2905
        $end  = $this->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true);
28✔
2906
        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
28✔
2907
        $name = trim($name);
28✔
2908

2909
        if ($name === '') {
28✔
2910
            return false;
2✔
2911
        }
2912

2913
        return $name;
26✔
2914

2915
    }//end findExtendedClassName()
2916

2917

2918
    /**
2919
     * Returns the names of the interfaces that the specified class or enum implements.
2920
     *
2921
     * Returns FALSE on error or if there are no implemented interface names.
2922
     *
2923
     * @param int $stackPtr The stack position of the class or enum token.
2924
     *
2925
     * @return array|false
2926
     */
2927
    public function findImplementedInterfaceNames($stackPtr)
34✔
2928
    {
2929
        // Check for the existence of the token.
2930
        if (isset($this->tokens[$stackPtr]) === false) {
34✔
2931
            return false;
2✔
2932
        }
2933

2934
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
32✔
2935
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
32✔
2936
            && $this->tokens[$stackPtr]['code'] !== T_ENUM
32✔
2937
        ) {
2938
            return false;
4✔
2939
        }
2940

2941
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
28✔
2942
            return false;
2✔
2943
        }
2944

2945
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
26✔
2946
        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
26✔
2947
        if ($implementsIndex === false) {
26✔
2948
            return false;
4✔
2949
        }
2950

2951
        $find   = Tokens::NAME_TOKENS;
22✔
2952
        $find[] = T_WHITESPACE;
22✔
2953
        $find[] = T_COMMA;
22✔
2954

2955
        $end  = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
22✔
2956
        $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
22✔
2957
        $name = trim($name);
22✔
2958

2959
        if ($name === '') {
22✔
2960
            return false;
2✔
2961
        } else {
2962
            $names = explode(',', $name);
20✔
2963
            $names = array_map('trim', $names);
20✔
2964
            return $names;
20✔
2965
        }
2966

2967
    }//end findImplementedInterfaceNames()
2968

2969

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