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

PHPCSStandards / PHP_CodeSniffer / 14312206404

07 Apr 2025 02:43PM UTC coverage: 78.707% (-0.001%) from 78.708%
14312206404

Pull #904

github

web-flow
Merge 386f8fa91 into 99818768c
Pull Request #904: Tests/Tokenizer: use markers for the `testSwitchDefault()` test

24855 of 31579 relevant lines covered (78.71%)

201.06 hits per line

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

71.95
/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\Util\Common;
18
use PHP_CodeSniffer\Util\Tokens;
19

20
class File
21
{
22

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

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

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

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

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

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

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

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

79
    /**
80
     * The name of the tokenizer being used for this file.
81
     *
82
     * @var string
83
     */
84
    public $tokenizerType = 'PHP';
85

86
    /**
87
     * Was the file loaded from cache?
88
     *
89
     * If TRUE, the file was loaded from a local cache.
90
     * If FALSE, the file was tokenized and processed fully.
91
     *
92
     * @var boolean
93
     */
94
    public $fromCache = false;
95

96
    /**
97
     * The number of tokens in this file.
98
     *
99
     * Stored here to save calling count() everywhere.
100
     *
101
     * @var integer
102
     */
103
    public $numTokens = 0;
104

105
    /**
106
     * The tokens stack map.
107
     *
108
     * @var array
109
     */
110
    protected $tokens = [];
111

112
    /**
113
     * The errors raised from sniffs.
114
     *
115
     * @var array
116
     * @see getErrors()
117
     */
118
    protected $errors = [];
119

120
    /**
121
     * The warnings raised from sniffs.
122
     *
123
     * @var array
124
     * @see getWarnings()
125
     */
126
    protected $warnings = [];
127

128
    /**
129
     * The metrics recorded by sniffs.
130
     *
131
     * @var array
132
     * @see getMetrics()
133
     */
134
    protected $metrics = [];
135

136
    /**
137
     * The metrics recorded for each token.
138
     *
139
     * Stops the same metric being recorded for the same token twice.
140
     *
141
     * @var array
142
     * @see getMetrics()
143
     */
144
    private $metricTokens = [];
145

146
    /**
147
     * The total number of errors raised.
148
     *
149
     * @var integer
150
     */
151
    protected $errorCount = 0;
152

153
    /**
154
     * The total number of warnings raised.
155
     *
156
     * @var integer
157
     */
158
    protected $warningCount = 0;
159

160
    /**
161
     * The total number of errors and warnings that can be fixed.
162
     *
163
     * @var integer
164
     */
165
    protected $fixableCount = 0;
166

167
    /**
168
     * The total number of errors and warnings that were fixed.
169
     *
170
     * @var integer
171
     */
172
    protected $fixedCount = 0;
173

174
    /**
175
     * TRUE if errors are being replayed from the cache.
176
     *
177
     * @var boolean
178
     */
179
    protected $replayingErrors = false;
180

181
    /**
182
     * An array of sniffs that are being ignored.
183
     *
184
     * @var array
185
     */
186
    protected $ignoredListeners = [];
187

188
    /**
189
     * An array of message codes that are being ignored.
190
     *
191
     * @var array
192
     */
193
    protected $ignoredCodes = [];
194

195
    /**
196
     * An array of sniffs listening to this file's processing.
197
     *
198
     * @var \PHP_CodeSniffer\Sniffs\Sniff[]
199
     */
200
    protected $listeners = [];
201

202
    /**
203
     * The class name of the sniff currently processing the file.
204
     *
205
     * @var string
206
     */
207
    protected $activeListener = '';
208

209
    /**
210
     * An array of sniffs being processed and how long they took.
211
     *
212
     * @var array
213
     * @see getListenerTimes()
214
     */
215
    protected $listenerTimes = [];
216

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

226

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

243
        $parts     = explode('.', $path);
×
244
        $extension = array_pop($parts);
×
245
        if (isset($config->extensions[$extension]) === true) {
×
246
            $this->tokenizerType = $config->extensions[$extension];
×
247
        } else {
248
            // Revert to default.
249
            $this->tokenizerType = 'PHP';
×
250
        }
251

252
        $this->configCache['cache']           = $this->config->cache;
×
253
        $this->configCache['sniffs']          = array_map('strtolower', $this->config->sniffs);
×
254
        $this->configCache['exclude']         = array_map('strtolower', $this->config->exclude);
×
255
        $this->configCache['errorSeverity']   = $this->config->errorSeverity;
×
256
        $this->configCache['warningSeverity'] = $this->config->warningSeverity;
×
257
        $this->configCache['recordErrors']    = $this->config->recordErrors;
×
258
        $this->configCache['trackTime']       = $this->config->trackTime;
×
259
        $this->configCache['ignorePatterns']  = $this->ruleset->ignorePatterns;
×
260
        $this->configCache['includePatterns'] = $this->ruleset->includePatterns;
×
261

262
    }//end __construct()
263

264

265
    /**
266
     * Set the content of the file.
267
     *
268
     * Setting the content also calculates the EOL char being used.
269
     *
270
     * @param string $content The file content.
271
     *
272
     * @return void
273
     */
274
    public function setContent($content)
×
275
    {
276
        $this->content = $content;
×
277
        $this->tokens  = [];
×
278

279
        try {
280
            $this->eolChar = Common::detectLineEndings($content);
×
281
        } catch (RuntimeException $e) {
×
282
            $this->addWarningOnLine($e->getMessage(), 1, 'Internal.DetectLineEndings');
×
283
            return;
×
284
        }
285

286
    }//end setContent()
287

288

289
    /**
290
     * Reloads the content of the file.
291
     *
292
     * By default, we have no idea where our content comes from,
293
     * so we can't do anything.
294
     *
295
     * @return void
296
     */
297
    public function reloadContent()
×
298
    {
299

300
    }//end reloadContent()
×
301

302

303
    /**
304
     * Disables caching of this file.
305
     *
306
     * @return void
307
     */
308
    public function disableCaching()
×
309
    {
310
        $this->configCache['cache'] = false;
×
311

312
    }//end disableCaching()
313

314

315
    /**
316
     * Starts the stack traversal and tells listeners when tokens are found.
317
     *
318
     * @return void
319
     */
320
    public function process()
×
321
    {
322
        if ($this->ignored === true) {
×
323
            return;
×
324
        }
325

326
        $this->errors       = [];
×
327
        $this->warnings     = [];
×
328
        $this->errorCount   = 0;
×
329
        $this->warningCount = 0;
×
330
        $this->fixableCount = 0;
×
331

332
        $this->parse();
×
333

334
        // Check if tokenizer errors cause this file to be ignored.
335
        if ($this->ignored === true) {
×
336
            return;
×
337
        }
338

339
        $this->fixer->startFile($this);
×
340

341
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
342
            echo "\t*** START TOKEN PROCESSING ***".PHP_EOL;
×
343
        }
344

345
        $foundCode        = false;
×
346
        $listenerIgnoreTo = [];
×
347
        $inTests          = defined('PHP_CODESNIFFER_IN_TESTS');
×
348
        $checkAnnotations = $this->config->annotations;
×
349

350
        // Foreach of the listeners that have registered to listen for this
351
        // token, get them to process it.
352
        foreach ($this->tokens as $stackPtr => $token) {
×
353
            // Check for ignored lines.
354
            if ($checkAnnotations === true
355
                && ($token['code'] === T_COMMENT
×
356
                || $token['code'] === T_PHPCS_IGNORE_FILE
×
357
                || $token['code'] === T_PHPCS_SET
×
358
                || $token['code'] === T_DOC_COMMENT_STRING
×
359
                || $token['code'] === T_DOC_COMMENT_TAG
×
360
                || ($inTests === true && $token['code'] === T_INLINE_HTML))
×
361
            ) {
362
                $commentText      = ltrim($this->tokens[$stackPtr]['content'], " \t/*#");
×
363
                $commentTextLower = strtolower($commentText);
×
364
                if (strpos($commentText, '@codingStandards') !== false) {
×
365
                    if (strpos($commentText, '@codingStandardsIgnoreFile') !== false) {
×
366
                        // Ignoring the whole file, just a little late.
367
                        $this->errors       = [];
×
368
                        $this->warnings     = [];
×
369
                        $this->errorCount   = 0;
×
370
                        $this->warningCount = 0;
×
371
                        $this->fixableCount = 0;
×
372
                        return;
×
373
                    } else if (strpos($commentText, '@codingStandardsChangeSetting') !== false) {
×
374
                        $start   = strpos($commentText, '@codingStandardsChangeSetting');
×
375
                        $comment = substr($commentText, ($start + 30));
×
376
                        $parts   = explode(' ', $comment);
×
377
                        if (count($parts) >= 2) {
×
378
                            $sniffParts = explode('.', $parts[0]);
×
379
                            if (count($sniffParts) >= 3) {
×
380
                                // If the sniff code is not known to us, it has not been registered in this run.
381
                                // But don't throw an error as it could be there for a different standard to use.
382
                                if (isset($this->ruleset->sniffCodes[$parts[0]]) === true) {
×
383
                                    $listenerCode  = array_shift($parts);
×
384
                                    $propertyCode  = array_shift($parts);
×
385
                                    $settings      = [
386
                                        'value' => rtrim(implode(' ', $parts), " */\r\n"),
×
387
                                        'scope' => 'sniff',
×
388
                                    ];
389
                                    $listenerClass = $this->ruleset->sniffCodes[$listenerCode];
×
390
                                    $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $settings);
×
391
                                }
392
                            }
393
                        }
394
                    }//end if
395
                } else if (substr($commentTextLower, 0, 16) === 'phpcs:ignorefile'
×
396
                    || substr($commentTextLower, 0, 17) === '@phpcs:ignorefile'
×
397
                ) {
398
                    // Ignoring the whole file, just a little late.
399
                    $this->errors       = [];
×
400
                    $this->warnings     = [];
×
401
                    $this->errorCount   = 0;
×
402
                    $this->warningCount = 0;
×
403
                    $this->fixableCount = 0;
×
404
                    return;
×
405
                } else if (substr($commentTextLower, 0, 9) === 'phpcs:set'
×
406
                    || substr($commentTextLower, 0, 10) === '@phpcs:set'
×
407
                ) {
408
                    if (isset($token['sniffCode']) === true) {
×
409
                        $listenerCode = $token['sniffCode'];
×
410
                        if (isset($this->ruleset->sniffCodes[$listenerCode]) === true) {
×
411
                            $propertyCode  = $token['sniffProperty'];
×
412
                            $settings      = [
413
                                'value' => $token['sniffPropertyValue'],
×
414
                                'scope' => 'sniff',
×
415
                            ];
416
                            $listenerClass = $this->ruleset->sniffCodes[$listenerCode];
×
417
                            $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $settings);
×
418
                        }
419
                    }
420
                }//end if
421
            }//end if
422

423
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
424
                $type    = $token['type'];
×
425
                $content = Common::prepareForOutput($token['content']);
×
426
                echo "\t\tProcess token $stackPtr: $type => $content".PHP_EOL;
×
427
            }
428

429
            if ($token['code'] !== T_INLINE_HTML) {
×
430
                $foundCode = true;
×
431
            }
432

433
            if (isset($this->ruleset->tokenListeners[$token['code']]) === false) {
×
434
                continue;
×
435
            }
436

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

446
                // Make sure this sniff supports the tokenizer
447
                // we are currently using.
448
                $class = $listenerData['class'];
×
449

450
                if (isset($listenerData['tokenizers'][$this->tokenizerType]) === false) {
×
451
                    continue;
×
452
                }
453

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

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

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

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

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

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

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

506
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
507
                    echo "\t\t\tProcessing ".$this->activeListener.'... ';
×
508
                }
509

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

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

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

524
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
525
                    $timeTaken = round(($timeTaken), 4);
×
526
                    echo "DONE in $timeTaken seconds".PHP_EOL;
×
527
                }
528

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

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

547
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
548
            echo "\t*** END TOKEN PROCESSING ***".PHP_EOL;
×
549
            echo "\t*** START SNIFF PROCESSING REPORT ***".PHP_EOL;
×
550

551
            arsort($this->listenerTimes, SORT_NUMERIC);
×
552
            foreach ($this->listenerTimes as $listener => $timeTaken) {
×
553
                echo "\t$listener: ".round(($timeTaken), 4).' secs'.PHP_EOL;
×
554
            }
555

556
            echo "\t*** END SNIFF PROCESSING REPORT ***".PHP_EOL;
×
557
        }
558

559
        $this->fixedCount += $this->fixer->getFixCount();
×
560

561
    }//end process()
562

563

564
    /**
565
     * Tokenizes the file and prepares it for the test run.
566
     *
567
     * @return void
568
     */
569
    public function parse()
×
570
    {
571
        if (empty($this->tokens) === false) {
×
572
            // File has already been parsed.
573
            return;
×
574
        }
575

576
        try {
577
            $tokenizerClass  = 'PHP_CodeSniffer\Tokenizers\\'.$this->tokenizerType;
×
578
            $this->tokenizer = new $tokenizerClass($this->content, $this->config, $this->eolChar);
×
579
            $this->tokens    = $this->tokenizer->getTokens();
×
580
        } catch (TokenizerException $e) {
×
581
            $this->ignored = true;
×
582
            $this->addWarning($e->getMessage(), null, 'Internal.Tokenizer.Exception');
×
583
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
584
                echo "[$this->tokenizerType => tokenizer error]... ";
×
585
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
586
                    echo PHP_EOL;
×
587
                }
588
            }
589

590
            return;
×
591
        }
592

593
        $this->numTokens = count($this->tokens);
×
594

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

609
        if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
610
            if ($this->numTokens === 0) {
×
611
                $numLines = 0;
×
612
            } else {
613
                $numLines = $this->tokens[($this->numTokens - 1)]['line'];
×
614
            }
615

616
            echo "[$this->tokenizerType => $this->numTokens tokens in $numLines lines]... ";
×
617
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
618
                echo PHP_EOL;
×
619
            }
620
        }
621

622
    }//end parse()
623

624

625
    /**
626
     * Returns the token stack for this file.
627
     *
628
     * @return array
629
     */
630
    public function getTokens()
×
631
    {
632
        return $this->tokens;
×
633

634
    }//end getTokens()
635

636

637
    /**
638
     * Remove vars stored in this file that are no longer required.
639
     *
640
     * @return void
641
     */
642
    public function cleanUp()
×
643
    {
644
        $this->listenerTimes = null;
×
645
        $this->content       = null;
×
646
        $this->tokens        = null;
×
647
        $this->metricTokens  = null;
×
648
        $this->tokenizer     = null;
×
649
        $this->fixer         = null;
×
650
        $this->config        = null;
×
651
        $this->ruleset       = null;
×
652

653
    }//end cleanUp()
654

655

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

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

687
    }//end addError()
688

689

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

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

721
    }//end addWarning()
722

723

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

745
    }//end addErrorOnLine()
746

747

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

769
    }//end addWarningOnLine()
770

771

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

798
        return false;
×
799

800
    }//end addFixableError()
801

802

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

829
        return false;
×
830

831
    }//end addFixableWarning()
832

833

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

856
        // Work out which sniff generated the message.
857
        $parts = explode('.', $code);
549✔
858
        if ($parts[0] === 'Internal') {
549✔
859
            // An internal message.
860
            $listenerCode = '';
×
861
            if ($this->activeListener !== '') {
×
862
                $listenerCode = Common::getSniffCode($this->activeListener);
×
863
            }
864

865
            $sniffCode  = $code;
×
866
            $checkCodes = [$sniffCode];
×
867
        } else {
868
            if ($parts[0] !== $code) {
549✔
869
                // The full message code has been passed in.
870
                $sniffCode    = $code;
×
871
                $listenerCode = substr($sniffCode, 0, strrpos($sniffCode, '.'));
×
872
            } else {
873
                $listenerCode = Common::getSniffCode($this->activeListener);
549✔
874
                $sniffCode    = $listenerCode.'.'.$code;
549✔
875
                $parts        = explode('.', $sniffCode);
549✔
876
            }
877

878
            $checkCodes = [
183✔
879
                $sniffCode,
549✔
880
                $parts[0].'.'.$parts[1].'.'.$parts[2],
549✔
881
                $parts[0].'.'.$parts[1],
549✔
882
                $parts[0],
549✔
883
            ];
366✔
884
        }//end if
885

886
        if (isset($this->tokenizer->ignoredLines[$line]) === true) {
549✔
887
            // Check if this line is ignoring this specific message.
888
            $ignored = false;
297✔
889
            foreach ($checkCodes as $checkCode) {
297✔
890
                if (isset($this->tokenizer->ignoredLines[$line][$checkCode]) === true) {
297✔
891
                    $ignored = true;
270✔
892
                    break;
270✔
893
                }
894
            }
99✔
895

896
            // If it is ignored, make sure there is no exception in place.
897
            if ($ignored === true
198✔
898
                && isset($this->tokenizer->ignoredLines[$line]['.except']) === true
297✔
899
            ) {
99✔
900
                foreach ($checkCodes as $checkCode) {
45✔
901
                    if (isset($this->tokenizer->ignoredLines[$line]['.except'][$checkCode]) === true) {
45✔
902
                        $ignored = false;
36✔
903
                        break;
36✔
904
                    }
905
                }
15✔
906
            }
15✔
907

908
            if ($ignored === true) {
297✔
909
                return false;
270✔
910
            }
911
        }//end if
75✔
912

913
        $includeAll = true;
513✔
914
        if ($this->configCache['cache'] === false
513✔
915
            || $this->configCache['recordErrors'] === false
513✔
916
        ) {
171✔
917
            $includeAll = false;
513✔
918
        }
171✔
919

920
        // Filter out any messages for sniffs that shouldn't have run
921
        // due to the use of the --sniffs command line argument.
922
        if ($includeAll === false
342✔
923
            && ((empty($this->configCache['sniffs']) === false
513✔
924
            && in_array(strtolower($listenerCode), $this->configCache['sniffs'], true) === false)
513✔
925
            || (empty($this->configCache['exclude']) === false
513✔
926
            && in_array(strtolower($listenerCode), $this->configCache['exclude'], true) === true))
513✔
927
        ) {
171✔
928
            return false;
×
929
        }
930

931
        // If we know this sniff code is being ignored for this file, return early.
932
        foreach ($checkCodes as $checkCode) {
513✔
933
            if (isset($this->ignoredCodes[$checkCode]) === true) {
513✔
934
                return false;
×
935
            }
936
        }
171✔
937

938
        $oppositeType = 'warning';
513✔
939
        if ($error === false) {
513✔
940
            $oppositeType = 'error';
234✔
941
        }
78✔
942

943
        foreach ($checkCodes as $checkCode) {
513✔
944
            // Make sure this message type has not been set to the opposite message type.
945
            if (isset($this->ruleset->ruleset[$checkCode]['type']) === true
513✔
946
                && $this->ruleset->ruleset[$checkCode]['type'] === $oppositeType
513✔
947
            ) {
171✔
948
                $error = !$error;
×
949
                break;
×
950
            }
951
        }
171✔
952

953
        if ($error === true) {
513✔
954
            $configSeverity = $this->configCache['errorSeverity'];
432✔
955
            $messageCount   = &$this->errorCount;
432✔
956
            $messages       = &$this->errors;
432✔
957
        } else {
144✔
958
            $configSeverity = $this->configCache['warningSeverity'];
234✔
959
            $messageCount   = &$this->warningCount;
234✔
960
            $messages       = &$this->warnings;
234✔
961
        }
962

963
        if ($includeAll === false && $configSeverity === 0) {
513✔
964
            // Don't bother doing any processing as these messages are just going to
965
            // be hidden in the reports anyway.
966
            return false;
×
967
        }
968

969
        if ($severity === 0) {
513✔
970
            $severity = 5;
513✔
971
        }
171✔
972

973
        foreach ($checkCodes as $checkCode) {
513✔
974
            // Make sure we are interested in this severity level.
975
            if (isset($this->ruleset->ruleset[$checkCode]['severity']) === true) {
513✔
976
                $severity = $this->ruleset->ruleset[$checkCode]['severity'];
×
977
                break;
×
978
            }
979
        }
171✔
980

981
        if ($includeAll === false && $configSeverity > $severity) {
513✔
982
            return false;
×
983
        }
984

985
        // Make sure we are not ignoring this file.
986
        $included = null;
513✔
987
        if (trim($this->path, '\'"') === 'STDIN') {
513✔
988
            $included = true;
513✔
989
        } else {
171✔
990
            foreach ($checkCodes as $checkCode) {
×
991
                $patterns = null;
×
992

993
                if (isset($this->configCache['includePatterns'][$checkCode]) === true) {
×
994
                    $patterns  = $this->configCache['includePatterns'][$checkCode];
×
995
                    $excluding = false;
×
996
                } else if (isset($this->configCache['ignorePatterns'][$checkCode]) === true) {
×
997
                    $patterns  = $this->configCache['ignorePatterns'][$checkCode];
×
998
                    $excluding = true;
×
999
                }
1000

1001
                if ($patterns === null) {
×
1002
                    continue;
×
1003
                }
1004

1005
                foreach ($patterns as $pattern => $type) {
×
1006
                    // While there is support for a type of each pattern
1007
                    // (absolute or relative) we don't actually support it here.
1008
                    $replacements = [
1009
                        '\\,' => ',',
×
1010
                        '*'   => '.*',
1011
                    ];
1012

1013
                    // We assume a / directory separator, as do the exclude rules
1014
                    // most developers write, so we need a special case for any system
1015
                    // that is different.
1016
                    if (DIRECTORY_SEPARATOR === '\\') {
×
1017
                        $replacements['/'] = '\\\\';
×
1018
                    }
1019

1020
                    $pattern = '`'.strtr($pattern, $replacements).'`i';
×
1021
                    $matched = preg_match($pattern, $this->path);
×
1022

1023
                    if ($matched === 0) {
×
1024
                        if ($excluding === false && $included === null) {
×
1025
                            // This file path is not being included.
1026
                            $included = false;
×
1027
                        }
1028

1029
                        continue;
×
1030
                    }
1031

1032
                    if ($excluding === true) {
×
1033
                        // This file path is being excluded.
1034
                        $this->ignoredCodes[$checkCode] = true;
×
1035
                        return false;
×
1036
                    }
1037

1038
                    // This file path is being included.
1039
                    $included = true;
×
1040
                    break;
×
1041
                }//end foreach
1042
            }//end foreach
1043
        }//end if
1044

1045
        if ($included === false) {
513✔
1046
            // There were include rules set, but this file
1047
            // path didn't match any of them.
1048
            return false;
×
1049
        }
1050

1051
        $messageCount++;
513✔
1052
        if ($fixable === true) {
513✔
1053
            $this->fixableCount++;
432✔
1054
        }
144✔
1055

1056
        if ($this->configCache['recordErrors'] === false
513✔
1057
            && $includeAll === false
513✔
1058
        ) {
171✔
1059
            return true;
×
1060
        }
1061

1062
        // See if there is a custom error message format to use.
1063
        // But don't do this if we are replaying errors because replayed
1064
        // errors have already used the custom format and have had their
1065
        // data replaced.
1066
        if ($this->replayingErrors === false
513✔
1067
            && isset($this->ruleset->ruleset[$sniffCode]['message']) === true
513✔
1068
        ) {
171✔
1069
            $message = $this->ruleset->ruleset[$sniffCode]['message'];
×
1070
        }
1071

1072
        if (empty($data) === false) {
513✔
1073
            $message = vsprintf($message, $data);
504✔
1074
        }
168✔
1075

1076
        if (isset($messages[$line]) === false) {
513✔
1077
            $messages[$line] = [];
513✔
1078
        }
171✔
1079

1080
        if (isset($messages[$line][$column]) === false) {
513✔
1081
            $messages[$line][$column] = [];
513✔
1082
        }
171✔
1083

1084
        $messages[$line][$column][] = [
513✔
1085
            'message'  => $message,
513✔
1086
            'source'   => $sniffCode,
513✔
1087
            'listener' => $this->activeListener,
513✔
1088
            'severity' => $severity,
513✔
1089
            'fixable'  => $fixable,
513✔
1090
        ];
171✔
1091

1092
        if (PHP_CODESNIFFER_VERBOSITY > 1
513✔
1093
            && $this->fixer->enabled === true
513✔
1094
            && $fixable === true
513✔
1095
        ) {
171✔
1096
            @ob_end_clean();
×
1097
            echo "\tE: [Line $line] $message ($sniffCode)".PHP_EOL;
×
1098
            ob_start();
×
1099
        }
1100

1101
        return true;
513✔
1102

1103
    }//end addMessage()
1104

1105

1106
    /**
1107
     * Record a metric about the file being examined.
1108
     *
1109
     * @param int    $stackPtr The stack position where the metric was recorded.
1110
     * @param string $metric   The name of the metric being recorded.
1111
     * @param string $value    The value of the metric being recorded.
1112
     *
1113
     * @return boolean
1114
     */
1115
    public function recordMetric($stackPtr, $metric, $value)
×
1116
    {
1117
        if (isset($this->metrics[$metric]) === false) {
×
1118
            $this->metrics[$metric] = ['values' => [$value => 1]];
×
1119
            $this->metricTokens[$metric][$stackPtr] = true;
×
1120
        } else if (isset($this->metricTokens[$metric][$stackPtr]) === false) {
×
1121
            $this->metricTokens[$metric][$stackPtr] = true;
×
1122
            if (isset($this->metrics[$metric]['values'][$value]) === false) {
×
1123
                $this->metrics[$metric]['values'][$value] = 1;
×
1124
            } else {
1125
                $this->metrics[$metric]['values'][$value]++;
×
1126
            }
1127
        }
1128

1129
        return true;
×
1130

1131
    }//end recordMetric()
1132

1133

1134
    /**
1135
     * Returns the number of errors raised.
1136
     *
1137
     * @return int
1138
     */
1139
    public function getErrorCount()
×
1140
    {
1141
        return $this->errorCount;
×
1142

1143
    }//end getErrorCount()
1144

1145

1146
    /**
1147
     * Returns the number of warnings raised.
1148
     *
1149
     * @return int
1150
     */
1151
    public function getWarningCount()
×
1152
    {
1153
        return $this->warningCount;
×
1154

1155
    }//end getWarningCount()
1156

1157

1158
    /**
1159
     * Returns the number of fixable errors/warnings raised.
1160
     *
1161
     * @return int
1162
     */
1163
    public function getFixableCount()
×
1164
    {
1165
        return $this->fixableCount;
×
1166

1167
    }//end getFixableCount()
1168

1169

1170
    /**
1171
     * Returns the number of fixed errors/warnings.
1172
     *
1173
     * @return int
1174
     */
1175
    public function getFixedCount()
×
1176
    {
1177
        return $this->fixedCount;
×
1178

1179
    }//end getFixedCount()
1180

1181

1182
    /**
1183
     * Returns the list of ignored lines.
1184
     *
1185
     * @return array
1186
     */
1187
    public function getIgnoredLines()
×
1188
    {
1189
        return $this->tokenizer->ignoredLines;
×
1190

1191
    }//end getIgnoredLines()
1192

1193

1194
    /**
1195
     * Returns the errors raised from processing this file.
1196
     *
1197
     * @return array
1198
     */
1199
    public function getErrors()
×
1200
    {
1201
        return $this->errors;
×
1202

1203
    }//end getErrors()
1204

1205

1206
    /**
1207
     * Returns the warnings raised from processing this file.
1208
     *
1209
     * @return array
1210
     */
1211
    public function getWarnings()
×
1212
    {
1213
        return $this->warnings;
×
1214

1215
    }//end getWarnings()
1216

1217

1218
    /**
1219
     * Returns the metrics found while processing this file.
1220
     *
1221
     * @return array
1222
     */
1223
    public function getMetrics()
×
1224
    {
1225
        return $this->metrics;
×
1226

1227
    }//end getMetrics()
1228

1229

1230
    /**
1231
     * Returns the time taken processing this file for each invoked sniff.
1232
     *
1233
     * @return array
1234
     */
1235
    public function getListenerTimes()
×
1236
    {
1237
        return $this->listenerTimes;
×
1238

1239
    }//end getListenerTimes()
1240

1241

1242
    /**
1243
     * Returns the absolute filename of this file.
1244
     *
1245
     * @return string
1246
     */
1247
    public function getFilename()
×
1248
    {
1249
        return $this->path;
×
1250

1251
    }//end getFilename()
1252

1253

1254
    /**
1255
     * Returns the declaration name for classes, interfaces, traits, enums, and functions.
1256
     *
1257
     * @param int $stackPtr The position of the declaration token which
1258
     *                      declared the class, interface, trait, or function.
1259
     *
1260
     * @return string|null The name of the class, interface, trait, or function;
1261
     *                     or NULL if the function or class is anonymous.
1262
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type
1263
     *                                                      T_FUNCTION, T_CLASS, T_ANON_CLASS,
1264
     *                                                      T_CLOSURE, T_TRAIT, T_ENUM, or T_INTERFACE.
1265
     */
1266
    public function getDeclarationName($stackPtr)
315✔
1267
    {
1268
        $tokenCode = $this->tokens[$stackPtr]['code'];
315✔
1269

1270
        if ($tokenCode === T_ANON_CLASS || $tokenCode === T_CLOSURE) {
315✔
1271
            return null;
54✔
1272
        }
1273

1274
        if ($tokenCode !== T_FUNCTION
174✔
1275
            && $tokenCode !== T_CLASS
261✔
1276
            && $tokenCode !== T_INTERFACE
261✔
1277
            && $tokenCode !== T_TRAIT
261✔
1278
            && $tokenCode !== T_ENUM
261✔
1279
        ) {
87✔
1280
            throw new RuntimeException('Token type "'.$this->tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM');
18✔
1281
        }
1282

1283
        if ($tokenCode === T_FUNCTION
162✔
1284
            && strtolower($this->tokens[$stackPtr]['content']) !== 'function'
243✔
1285
        ) {
81✔
1286
            // This is a function declared without the "function" keyword.
1287
            // So this token is the function name.
1288
            return $this->tokens[$stackPtr]['content'];
9✔
1289
        }
1290

1291
        $stopPoint = $this->numTokens;
234✔
1292
        if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === true) {
234✔
1293
            // For functions, stop searching at the parenthesis opener.
1294
            $stopPoint = $this->tokens[$stackPtr]['parenthesis_opener'];
126✔
1295
        } else if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
150✔
1296
            // For OO tokens, stop searching at the open curly.
1297
            $stopPoint = $this->tokens[$stackPtr]['scope_opener'];
99✔
1298
        }
33✔
1299

1300
        $content = null;
234✔
1301
        for ($i = $stackPtr; $i < $stopPoint; $i++) {
234✔
1302
            if ($this->tokens[$i]['code'] === T_STRING) {
234✔
1303
                $content = $this->tokens[$i]['content'];
216✔
1304
                break;
216✔
1305
            }
1306
        }
78✔
1307

1308
        return $content;
234✔
1309

1310
    }//end getDeclarationName()
1311

1312

1313
    /**
1314
     * Returns the method parameters for the specified function token.
1315
     *
1316
     * Also supports passing in a USE token for a closure use group.
1317
     *
1318
     * Each parameter is in the following format:
1319
     *
1320
     * <code>
1321
     *   0 => array(
1322
     *         'name'                => string,        // The variable name.
1323
     *         'token'               => integer,       // The stack pointer to the variable name.
1324
     *         'content'             => string,        // The full content of the variable definition.
1325
     *         'has_attributes'      => boolean,       // Does the parameter have one or more attributes attached ?
1326
     *         'pass_by_reference'   => boolean,       // Is the variable passed by reference?
1327
     *         'reference_token'     => integer|false, // The stack pointer to the reference operator
1328
     *                                                 // or FALSE if the param is not passed by reference.
1329
     *         'variable_length'     => boolean,       // Is the param of variable length through use of `...` ?
1330
     *         'variadic_token'      => integer|false, // The stack pointer to the ... operator
1331
     *                                                 // or FALSE if the param is not variable length.
1332
     *         'type_hint'           => string,        // The type hint for the variable.
1333
     *         'type_hint_token'     => integer|false, // The stack pointer to the start of the type hint
1334
     *                                                 // or FALSE if there is no type hint.
1335
     *         'type_hint_end_token' => integer|false, // The stack pointer to the end of the type hint
1336
     *                                                 // or FALSE if there is no type hint.
1337
     *         'nullable_type'       => boolean,       // TRUE if the type is preceded by the nullability
1338
     *                                                 // operator.
1339
     *         'comma_token'         => integer|false, // The stack pointer to the comma after the param
1340
     *                                                 // or FALSE if this is the last param.
1341
     *        )
1342
     * </code>
1343
     *
1344
     * Parameters with default values have additional array indexes of:
1345
     *         'default'             => string,  // The full content of the default value.
1346
     *         'default_token'       => integer, // The stack pointer to the start of the default value.
1347
     *         'default_equal_token' => integer, // The stack pointer to the equals sign.
1348
     *
1349
     * Parameters declared using PHP 8 constructor property promotion, have these additional array indexes:
1350
     *         'property_visibility' => string,        // The property visibility as declared.
1351
     *         'visibility_token'    => integer|false, // The stack pointer to the visibility modifier token
1352
     *                                                 // or FALSE if the visibility is not explicitly declared.
1353
     *         'property_readonly'   => boolean,       // TRUE if the readonly keyword was found.
1354
     *         'readonly_token'      => integer,       // The stack pointer to the readonly modifier token.
1355
     *                                                 // This index will only be set if the property is readonly.
1356
     *
1357
     * @param int $stackPtr The position in the stack of the function token
1358
     *                      to acquire the parameters for.
1359
     *
1360
     * @return array
1361
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified $stackPtr is not of
1362
     *                                                      type T_FUNCTION, T_CLOSURE, T_USE,
1363
     *                                                      or T_FN.
1364
     */
1365
    public function getMethodParameters($stackPtr)
693✔
1366
    {
1367
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
693✔
1368
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
693✔
1369
            && $this->tokens[$stackPtr]['code'] !== T_USE
693✔
1370
            && $this->tokens[$stackPtr]['code'] !== T_FN
693✔
1371
        ) {
231✔
1372
            throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_USE or T_FN');
27✔
1373
        }
1374

1375
        if ($this->tokens[$stackPtr]['code'] === T_USE) {
666✔
1376
            $opener = $this->findNext(T_OPEN_PARENTHESIS, ($stackPtr + 1));
63✔
1377
            if ($opener === false || isset($this->tokens[$opener]['parenthesis_owner']) === true) {
63✔
1378
                throw new RuntimeException('$stackPtr was not a valid T_USE');
39✔
1379
            }
1380
        } else {
12✔
1381
            if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === false) {
603✔
1382
                // Live coding or syntax error, so no params to find.
1383
                return [];
9✔
1384
            }
1385

1386
            $opener = $this->tokens[$stackPtr]['parenthesis_opener'];
594✔
1387
        }
1388

1389
        if (isset($this->tokens[$opener]['parenthesis_closer']) === false) {
630✔
1390
            // Live coding or syntax error, so no params to find.
1391
            return [];
9✔
1392
        }
1393

1394
        $closer = $this->tokens[$opener]['parenthesis_closer'];
621✔
1395

1396
        $vars            = [];
621✔
1397
        $currVar         = null;
621✔
1398
        $paramStart      = ($opener + 1);
621✔
1399
        $defaultStart    = null;
621✔
1400
        $equalToken      = null;
621✔
1401
        $paramCount      = 0;
621✔
1402
        $hasAttributes   = false;
621✔
1403
        $passByReference = false;
621✔
1404
        $referenceToken  = false;
621✔
1405
        $variableLength  = false;
621✔
1406
        $variadicToken   = false;
621✔
1407
        $typeHint        = '';
621✔
1408
        $typeHintToken   = false;
621✔
1409
        $typeHintEndToken = false;
621✔
1410
        $nullableType     = false;
621✔
1411
        $visibilityToken  = null;
621✔
1412
        $readonlyToken    = null;
621✔
1413

1414
        for ($i = $paramStart; $i <= $closer; $i++) {
621✔
1415
            // Check to see if this token has a parenthesis or bracket opener. If it does
1416
            // it's likely to be an array which might have arguments in it. This
1417
            // could cause problems in our parsing below, so lets just skip to the
1418
            // end of it.
1419
            if ($this->tokens[$i]['code'] !== T_TYPE_OPEN_PARENTHESIS
621✔
1420
                && isset($this->tokens[$i]['parenthesis_opener']) === true
621✔
1421
            ) {
207✔
1422
                // Don't do this if it's the close parenthesis for the method.
1423
                if ($i !== $this->tokens[$i]['parenthesis_closer']) {
621✔
1424
                    $i = $this->tokens[$i]['parenthesis_closer'];
27✔
1425
                    continue;
27✔
1426
                }
1427
            }
207✔
1428

1429
            if (isset($this->tokens[$i]['bracket_opener']) === true) {
621✔
1430
                if ($i !== $this->tokens[$i]['bracket_closer']) {
9✔
1431
                    $i = $this->tokens[$i]['bracket_closer'];
9✔
1432
                    continue;
9✔
1433
                }
1434
            }
1435

1436
            switch ($this->tokens[$i]['code']) {
621✔
1437
            case T_ATTRIBUTE:
621✔
1438
                $hasAttributes = true;
18✔
1439

1440
                // Skip to the end of the attribute.
1441
                $i = $this->tokens[$i]['attribute_closer'];
18✔
1442
                break;
18✔
1443
            case T_BITWISE_AND:
621✔
1444
                if ($defaultStart === null) {
162✔
1445
                    $passByReference = true;
153✔
1446
                    $referenceToken  = $i;
153✔
1447
                }
51✔
1448
                break;
162✔
1449
            case T_VARIABLE:
621✔
1450
                $currVar = $i;
594✔
1451
                break;
594✔
1452
            case T_ELLIPSIS:
621✔
1453
                $variableLength = true;
153✔
1454
                $variadicToken  = $i;
153✔
1455
                break;
153✔
1456
            case T_CALLABLE:
621✔
1457
                if ($typeHintToken === false) {
36✔
1458
                    $typeHintToken = $i;
27✔
1459
                }
9✔
1460

1461
                $typeHint        .= $this->tokens[$i]['content'];
36✔
1462
                $typeHintEndToken = $i;
36✔
1463
                break;
36✔
1464
            case T_SELF:
621✔
1465
            case T_PARENT:
621✔
1466
            case T_STATIC:
621✔
1467
                // Self and parent are valid, static invalid, but was probably intended as type hint.
1468
                if (isset($defaultStart) === false) {
54✔
1469
                    if ($typeHintToken === false) {
45✔
1470
                        $typeHintToken = $i;
36✔
1471
                    }
12✔
1472

1473
                    $typeHint        .= $this->tokens[$i]['content'];
45✔
1474
                    $typeHintEndToken = $i;
45✔
1475
                }
15✔
1476
                break;
54✔
1477
            case T_STRING:
621✔
1478
                // This is a string, so it may be a type hint, but it could
1479
                // also be a constant used as a default value.
1480
                $prevComma = false;
414✔
1481
                for ($t = $i; $t >= $opener; $t--) {
414✔
1482
                    if ($this->tokens[$t]['code'] === T_COMMA) {
414✔
1483
                        $prevComma = $t;
180✔
1484
                        break;
180✔
1485
                    }
1486
                }
138✔
1487

1488
                if ($prevComma !== false) {
414✔
1489
                    $nextEquals = false;
180✔
1490
                    for ($t = $prevComma; $t < $i; $t++) {
180✔
1491
                        if ($this->tokens[$t]['code'] === T_EQUAL) {
180✔
1492
                            $nextEquals = $t;
27✔
1493
                            break;
27✔
1494
                        }
1495
                    }
60✔
1496

1497
                    if ($nextEquals !== false) {
180✔
1498
                        break;
27✔
1499
                    }
1500
                }
57✔
1501

1502
                if ($defaultStart === null) {
405✔
1503
                    if ($typeHintToken === false) {
396✔
1504
                        $typeHintToken = $i;
342✔
1505
                    }
114✔
1506

1507
                    $typeHint        .= $this->tokens[$i]['content'];
396✔
1508
                    $typeHintEndToken = $i;
396✔
1509
                }
132✔
1510
                break;
405✔
1511
            case T_NAMESPACE:
621✔
1512
            case T_NS_SEPARATOR:
621✔
1513
            case T_TYPE_UNION:
621✔
1514
            case T_TYPE_INTERSECTION:
621✔
1515
            case T_TYPE_OPEN_PARENTHESIS:
621✔
1516
            case T_TYPE_CLOSE_PARENTHESIS:
621✔
1517
            case T_FALSE:
621✔
1518
            case T_TRUE:
621✔
1519
            case T_NULL:
621✔
1520
                // Part of a type hint or default value.
1521
                if ($defaultStart === null) {
324✔
1522
                    if ($typeHintToken === false) {
297✔
1523
                        $typeHintToken = $i;
126✔
1524
                    }
42✔
1525

1526
                    $typeHint        .= $this->tokens[$i]['content'];
297✔
1527
                    $typeHintEndToken = $i;
297✔
1528
                }
99✔
1529
                break;
324✔
1530
            case T_NULLABLE:
621✔
1531
                if ($defaultStart === null) {
189✔
1532
                    $nullableType     = true;
189✔
1533
                    $typeHint        .= $this->tokens[$i]['content'];
189✔
1534
                    $typeHintEndToken = $i;
189✔
1535
                }
63✔
1536
                break;
189✔
1537
            case T_PUBLIC:
621✔
1538
            case T_PROTECTED:
621✔
1539
            case T_PRIVATE:
621✔
1540
                if ($defaultStart === null) {
72✔
1541
                    $visibilityToken = $i;
72✔
1542
                }
24✔
1543
                break;
72✔
1544
            case T_READONLY:
621✔
1545
                if ($defaultStart === null) {
27✔
1546
                    $readonlyToken = $i;
27✔
1547
                }
9✔
1548
                break;
27✔
1549
            case T_CLOSE_PARENTHESIS:
621✔
1550
            case T_COMMA:
591✔
1551
                // If it's null, then there must be no parameters for this
1552
                // method.
1553
                if ($currVar === null) {
621✔
1554
                    continue 2;
99✔
1555
                }
1556

1557
                $vars[$paramCount]            = [];
594✔
1558
                $vars[$paramCount]['token']   = $currVar;
594✔
1559
                $vars[$paramCount]['name']    = $this->tokens[$currVar]['content'];
594✔
1560
                $vars[$paramCount]['content'] = trim($this->getTokensAsString($paramStart, ($i - $paramStart)));
594✔
1561

1562
                if ($defaultStart !== null) {
594✔
1563
                    $vars[$paramCount]['default']       = trim($this->getTokensAsString($defaultStart, ($i - $defaultStart)));
198✔
1564
                    $vars[$paramCount]['default_token'] = $defaultStart;
198✔
1565
                    $vars[$paramCount]['default_equal_token'] = $equalToken;
198✔
1566
                }
66✔
1567

1568
                $vars[$paramCount]['has_attributes']      = $hasAttributes;
594✔
1569
                $vars[$paramCount]['pass_by_reference']   = $passByReference;
594✔
1570
                $vars[$paramCount]['reference_token']     = $referenceToken;
594✔
1571
                $vars[$paramCount]['variable_length']     = $variableLength;
594✔
1572
                $vars[$paramCount]['variadic_token']      = $variadicToken;
594✔
1573
                $vars[$paramCount]['type_hint']           = $typeHint;
594✔
1574
                $vars[$paramCount]['type_hint_token']     = $typeHintToken;
594✔
1575
                $vars[$paramCount]['type_hint_end_token'] = $typeHintEndToken;
594✔
1576
                $vars[$paramCount]['nullable_type']       = $nullableType;
594✔
1577

1578
                if ($visibilityToken !== null || $readonlyToken !== null) {
594✔
1579
                    $vars[$paramCount]['property_visibility'] = 'public';
81✔
1580
                    $vars[$paramCount]['visibility_token']    = false;
81✔
1581
                    $vars[$paramCount]['property_readonly']   = false;
81✔
1582

1583
                    if ($visibilityToken !== null) {
81✔
1584
                        $vars[$paramCount]['property_visibility'] = $this->tokens[$visibilityToken]['content'];
72✔
1585
                        $vars[$paramCount]['visibility_token']    = $visibilityToken;
72✔
1586
                    }
24✔
1587

1588
                    if ($readonlyToken !== null) {
81✔
1589
                        $vars[$paramCount]['property_readonly'] = true;
27✔
1590
                        $vars[$paramCount]['readonly_token']    = $readonlyToken;
27✔
1591
                    }
9✔
1592
                }
27✔
1593

1594
                if ($this->tokens[$i]['code'] === T_COMMA) {
594✔
1595
                    $vars[$paramCount]['comma_token'] = $i;
297✔
1596
                } else {
99✔
1597
                    $vars[$paramCount]['comma_token'] = false;
522✔
1598
                }
1599

1600
                // Reset the vars, as we are about to process the next parameter.
1601
                $currVar          = null;
594✔
1602
                $paramStart       = ($i + 1);
594✔
1603
                $defaultStart     = null;
594✔
1604
                $equalToken       = null;
594✔
1605
                $hasAttributes    = false;
594✔
1606
                $passByReference  = false;
594✔
1607
                $referenceToken   = false;
594✔
1608
                $variableLength   = false;
594✔
1609
                $variadicToken    = false;
594✔
1610
                $typeHint         = '';
594✔
1611
                $typeHintToken    = false;
594✔
1612
                $typeHintEndToken = false;
594✔
1613
                $nullableType     = false;
594✔
1614
                $visibilityToken  = null;
594✔
1615
                $readonlyToken    = null;
594✔
1616

1617
                $paramCount++;
594✔
1618
                break;
594✔
1619
            case T_EQUAL:
576✔
1620
                $defaultStart = $this->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
198✔
1621
                $equalToken   = $i;
198✔
1622
                break;
198✔
1623
            }//end switch
198✔
1624
        }//end for
198✔
1625

1626
        return $vars;
621✔
1627

1628
    }//end getMethodParameters()
1629

1630

1631
    /**
1632
     * Returns the visibility and implementation properties of a method.
1633
     *
1634
     * The format of the return value is:
1635
     * <code>
1636
     *   array(
1637
     *    'scope'                 => string,        // Public, private, or protected
1638
     *    'scope_specified'       => boolean,       // TRUE if the scope keyword was found.
1639
     *    'return_type'           => string,        // The return type of the method.
1640
     *    'return_type_token'     => integer|false, // The stack pointer to the start of the return type
1641
     *                                              // or FALSE if there is no return type.
1642
     *    'return_type_end_token' => integer|false, // The stack pointer to the end of the return type
1643
     *                                              // or FALSE if there is no return type.
1644
     *    'nullable_return_type'  => boolean,       // TRUE if the return type is preceded by the
1645
     *                                              // nullability operator.
1646
     *    'is_abstract'           => boolean,       // TRUE if the abstract keyword was found.
1647
     *    'is_final'              => boolean,       // TRUE if the final keyword was found.
1648
     *    'is_static'             => boolean,       // TRUE if the static keyword was found.
1649
     *    'has_body'              => boolean,       // TRUE if the method has a body
1650
     *   );
1651
     * </code>
1652
     *
1653
     * @param int $stackPtr The position in the stack of the function token to
1654
     *                      acquire the properties for.
1655
     *
1656
     * @return array
1657
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1658
     *                                                      T_FUNCTION, T_CLOSURE, or T_FN token.
1659
     */
1660
    public function getMethodProperties($stackPtr)
531✔
1661
    {
1662
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
531✔
1663
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
531✔
1664
            && $this->tokens[$stackPtr]['code'] !== T_FN
531✔
1665
        ) {
177✔
1666
            throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_FN');
27✔
1667
        }
1668

1669
        if ($this->tokens[$stackPtr]['code'] === T_FUNCTION) {
504✔
1670
            $valid = [
123✔
1671
                T_PUBLIC      => T_PUBLIC,
369✔
1672
                T_PRIVATE     => T_PRIVATE,
369✔
1673
                T_PROTECTED   => T_PROTECTED,
369✔
1674
                T_STATIC      => T_STATIC,
369✔
1675
                T_FINAL       => T_FINAL,
369✔
1676
                T_ABSTRACT    => T_ABSTRACT,
369✔
1677
                T_WHITESPACE  => T_WHITESPACE,
369✔
1678
                T_COMMENT     => T_COMMENT,
369✔
1679
                T_DOC_COMMENT => T_DOC_COMMENT,
369✔
1680
            ];
246✔
1681
        } else {
123✔
1682
            $valid = [
45✔
1683
                T_STATIC      => T_STATIC,
135✔
1684
                T_WHITESPACE  => T_WHITESPACE,
135✔
1685
                T_COMMENT     => T_COMMENT,
135✔
1686
                T_DOC_COMMENT => T_DOC_COMMENT,
135✔
1687
            ];
90✔
1688
        }
1689

1690
        $scope          = 'public';
504✔
1691
        $scopeSpecified = false;
504✔
1692
        $isAbstract     = false;
504✔
1693
        $isFinal        = false;
504✔
1694
        $isStatic       = false;
504✔
1695

1696
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
504✔
1697
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
504✔
1698
                break;
495✔
1699
            }
1700

1701
            switch ($this->tokens[$i]['code']) {
495✔
1702
            case T_PUBLIC:
495✔
1703
                $scope          = 'public';
54✔
1704
                $scopeSpecified = true;
54✔
1705
                break;
54✔
1706
            case T_PRIVATE:
495✔
1707
                $scope          = 'private';
27✔
1708
                $scopeSpecified = true;
27✔
1709
                break;
27✔
1710
            case T_PROTECTED:
495✔
1711
                $scope          = 'protected';
27✔
1712
                $scopeSpecified = true;
27✔
1713
                break;
27✔
1714
            case T_ABSTRACT:
495✔
1715
                $isAbstract = true;
27✔
1716
                break;
27✔
1717
            case T_FINAL:
495✔
1718
                $isFinal = true;
9✔
1719
                break;
9✔
1720
            case T_STATIC:
495✔
1721
                $isStatic = true;
18✔
1722
                break;
18✔
1723
            }//end switch
165✔
1724
        }//end for
165✔
1725

1726
        $returnType         = '';
504✔
1727
        $returnTypeToken    = false;
504✔
1728
        $returnTypeEndToken = false;
504✔
1729
        $nullableReturnType = false;
504✔
1730
        $hasBody            = true;
504✔
1731

1732
        if (isset($this->tokens[$stackPtr]['parenthesis_closer']) === true) {
504✔
1733
            $scopeOpener = null;
504✔
1734
            if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
504✔
1735
                $scopeOpener = $this->tokens[$stackPtr]['scope_opener'];
459✔
1736
            }
153✔
1737

1738
            $valid = [
168✔
1739
                T_STRING                 => T_STRING,
504✔
1740
                T_CALLABLE               => T_CALLABLE,
504✔
1741
                T_SELF                   => T_SELF,
504✔
1742
                T_PARENT                 => T_PARENT,
504✔
1743
                T_STATIC                 => T_STATIC,
504✔
1744
                T_FALSE                  => T_FALSE,
504✔
1745
                T_TRUE                   => T_TRUE,
504✔
1746
                T_NULL                   => T_NULL,
504✔
1747
                T_NAMESPACE              => T_NAMESPACE,
504✔
1748
                T_NS_SEPARATOR           => T_NS_SEPARATOR,
504✔
1749
                T_TYPE_UNION             => T_TYPE_UNION,
504✔
1750
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
504✔
1751
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
504✔
1752
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
504✔
1753
            ];
336✔
1754

1755
            for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) {
504✔
1756
                if (($scopeOpener === null && $this->tokens[$i]['code'] === T_SEMICOLON)
504✔
1757
                    || ($scopeOpener !== null && $i === $scopeOpener)
504✔
1758
                ) {
168✔
1759
                    // End of function definition.
1760
                    break;
504✔
1761
                }
1762

1763
                if ($this->tokens[$i]['code'] === T_USE) {
504✔
1764
                    // Skip over closure use statements.
1765
                    for ($j = ($i + 1); $j < $this->numTokens && isset(Tokens::$emptyTokens[$this->tokens[$j]['code']]) === true; $j++);
45✔
1766
                    if ($this->tokens[$j]['code'] === T_OPEN_PARENTHESIS) {
45✔
1767
                        if (isset($this->tokens[$j]['parenthesis_closer']) === false) {
45✔
1768
                            // Live coding/parse error, stop parsing.
1769
                            break;
×
1770
                        }
1771

1772
                        $i = $this->tokens[$j]['parenthesis_closer'];
45✔
1773
                        continue;
45✔
1774
                    }
1775
                }
1776

1777
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
504✔
1778
                    $nullableReturnType = true;
117✔
1779
                }
39✔
1780

1781
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
504✔
1782
                    if ($returnTypeToken === false) {
432✔
1783
                        $returnTypeToken = $i;
432✔
1784
                    }
144✔
1785

1786
                    $returnType        .= $this->tokens[$i]['content'];
432✔
1787
                    $returnTypeEndToken = $i;
432✔
1788
                }
144✔
1789
            }//end for
168✔
1790

1791
            if ($this->tokens[$stackPtr]['code'] === T_FN) {
504✔
1792
                $bodyToken = T_FN_ARROW;
54✔
1793
            } else {
18✔
1794
                $bodyToken = T_OPEN_CURLY_BRACKET;
450✔
1795
            }
1796

1797
            $end     = $this->findNext([$bodyToken, T_SEMICOLON], $this->tokens[$stackPtr]['parenthesis_closer']);
504✔
1798
            $hasBody = $this->tokens[$end]['code'] === $bodyToken;
504✔
1799
        }//end if
168✔
1800

1801
        if ($returnType !== '' && $nullableReturnType === true) {
504✔
1802
            $returnType = '?'.$returnType;
117✔
1803
        }
39✔
1804

1805
        return [
168✔
1806
            'scope'                 => $scope,
504✔
1807
            'scope_specified'       => $scopeSpecified,
504✔
1808
            'return_type'           => $returnType,
504✔
1809
            'return_type_token'     => $returnTypeToken,
504✔
1810
            'return_type_end_token' => $returnTypeEndToken,
504✔
1811
            'nullable_return_type'  => $nullableReturnType,
504✔
1812
            'is_abstract'           => $isAbstract,
504✔
1813
            'is_final'              => $isFinal,
504✔
1814
            'is_static'             => $isStatic,
504✔
1815
            'has_body'              => $hasBody,
504✔
1816
        ];
336✔
1817

1818
    }//end getMethodProperties()
1819

1820

1821
    /**
1822
     * Returns the visibility and implementation properties of a class member var.
1823
     *
1824
     * The format of the return value is:
1825
     *
1826
     * <code>
1827
     *   array(
1828
     *    'scope'           => string,        // Public, private, or protected.
1829
     *    'scope_specified' => boolean,       // TRUE if the scope was explicitly specified.
1830
     *    'is_static'       => boolean,       // TRUE if the static keyword was found.
1831
     *    'is_readonly'     => boolean,       // TRUE if the readonly keyword was found.
1832
     *    'is_final'        => boolean,       // TRUE if the final keyword was found.
1833
     *    'type'            => string,        // The type of the var (empty if no type specified).
1834
     *    'type_token'      => integer|false, // The stack pointer to the start of the type
1835
     *                                        // or FALSE if there is no type.
1836
     *    'type_end_token'  => integer|false, // The stack pointer to the end of the type
1837
     *                                        // or FALSE if there is no type.
1838
     *    'nullable_type'   => boolean,       // TRUE if the type is preceded by the nullability
1839
     *                                        // operator.
1840
     *   );
1841
     * </code>
1842
     *
1843
     * @param int $stackPtr The position in the stack of the T_VARIABLE token to
1844
     *                      acquire the properties for.
1845
     *
1846
     * @return array
1847
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1848
     *                                                      T_VARIABLE token, or if the position is not
1849
     *                                                      a class member variable.
1850
     */
1851
    public function getMemberProperties($stackPtr)
900✔
1852
    {
1853
        if ($this->tokens[$stackPtr]['code'] !== T_VARIABLE) {
900✔
1854
            throw new RuntimeException('$stackPtr must be of type T_VARIABLE');
9✔
1855
        }
1856

1857
        $conditions = array_keys($this->tokens[$stackPtr]['conditions']);
891✔
1858
        $ptr        = array_pop($conditions);
891✔
1859
        if (isset($this->tokens[$ptr]) === false
891✔
1860
            || ($this->tokens[$ptr]['code'] !== T_CLASS
888✔
1861
            && $this->tokens[$ptr]['code'] !== T_ANON_CLASS
885✔
1862
            && $this->tokens[$ptr]['code'] !== T_TRAIT)
888✔
1863
        ) {
297✔
1864
            if (isset($this->tokens[$ptr]) === true
54✔
1865
                && ($this->tokens[$ptr]['code'] === T_INTERFACE
51✔
1866
                || $this->tokens[$ptr]['code'] === T_ENUM)
51✔
1867
            ) {
18✔
1868
                // T_VARIABLEs in interfaces/enums can actually be method arguments
1869
                // but they won't be seen as being inside the method because there
1870
                // are no scope openers and closers for abstract methods. If it is in
1871
                // parentheses, we can be pretty sure it is a method argument.
1872
                if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === false
27✔
1873
                    || empty($this->tokens[$stackPtr]['nested_parenthesis']) === true
27✔
1874
                ) {
9✔
1875
                    $error = 'Possible parse error: %ss may not include member vars';
18✔
1876
                    $code  = sprintf('Internal.ParseError.%sHasMemberVar', ucfirst($this->tokens[$ptr]['content']));
18✔
1877
                    $data  = [strtolower($this->tokens[$ptr]['content'])];
18✔
1878
                    $this->addWarning($error, $stackPtr, $code, $data);
18✔
1879
                    return [];
21✔
1880
                }
1881
            } else {
3✔
1882
                throw new RuntimeException('$stackPtr is not a class member var');
27✔
1883
            }
1884
        }//end if
3✔
1885

1886
        // Make sure it's not a method parameter.
1887
        if (empty($this->tokens[$stackPtr]['nested_parenthesis']) === false) {
846✔
1888
            $parenthesis = array_keys($this->tokens[$stackPtr]['nested_parenthesis']);
54✔
1889
            $deepestOpen = array_pop($parenthesis);
54✔
1890
            if ($deepestOpen > $ptr
36✔
1891
                && isset($this->tokens[$deepestOpen]['parenthesis_owner']) === true
54✔
1892
                && $this->tokens[$this->tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION
54✔
1893
            ) {
18✔
1894
                throw new RuntimeException('$stackPtr is not a class member var');
36✔
1895
            }
1896
        }
6✔
1897

1898
        $valid = [
270✔
1899
            T_PUBLIC    => T_PUBLIC,
810✔
1900
            T_PRIVATE   => T_PRIVATE,
810✔
1901
            T_PROTECTED => T_PROTECTED,
810✔
1902
            T_STATIC    => T_STATIC,
810✔
1903
            T_VAR       => T_VAR,
810✔
1904
            T_READONLY  => T_READONLY,
810✔
1905
            T_FINAL     => T_FINAL,
810✔
1906
        ];
540✔
1907

1908
        $valid += Tokens::$emptyTokens;
810✔
1909

1910
        $scope          = 'public';
810✔
1911
        $scopeSpecified = false;
810✔
1912
        $isStatic       = false;
810✔
1913
        $isReadonly     = false;
810✔
1914
        $isFinal        = false;
810✔
1915

1916
        $startOfStatement = $this->findPrevious(
810✔
1917
            [
270✔
1918
                T_SEMICOLON,
810✔
1919
                T_OPEN_CURLY_BRACKET,
810✔
1920
                T_CLOSE_CURLY_BRACKET,
810✔
1921
                T_ATTRIBUTE_END,
810✔
1922
            ],
540✔
1923
            ($stackPtr - 1)
810✔
1924
        );
540✔
1925

1926
        for ($i = ($startOfStatement + 1); $i < $stackPtr; $i++) {
810✔
1927
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
810✔
1928
                break;
621✔
1929
            }
1930

1931
            switch ($this->tokens[$i]['code']) {
810✔
1932
            case T_PUBLIC:
810✔
1933
                $scope          = 'public';
387✔
1934
                $scopeSpecified = true;
387✔
1935
                break;
387✔
1936
            case T_PRIVATE:
810✔
1937
                $scope          = 'private';
171✔
1938
                $scopeSpecified = true;
171✔
1939
                break;
171✔
1940
            case T_PROTECTED:
810✔
1941
                $scope          = 'protected';
126✔
1942
                $scopeSpecified = true;
126✔
1943
                break;
126✔
1944
            case T_STATIC:
810✔
1945
                $isStatic = true;
198✔
1946
                break;
198✔
1947
            case T_READONLY:
810✔
1948
                $isReadonly = true;
108✔
1949
                break;
108✔
1950
            case T_FINAL:
810✔
1951
                $isFinal = true;
81✔
1952
                break;
81✔
1953
            }//end switch
270✔
1954
        }//end for
270✔
1955

1956
        $type         = '';
810✔
1957
        $typeToken    = false;
810✔
1958
        $typeEndToken = false;
810✔
1959
        $nullableType = false;
810✔
1960

1961
        if ($i < $stackPtr) {
810✔
1962
            // We've found a type.
1963
            $valid = [
207✔
1964
                T_STRING                 => T_STRING,
621✔
1965
                T_CALLABLE               => T_CALLABLE,
621✔
1966
                T_SELF                   => T_SELF,
621✔
1967
                T_PARENT                 => T_PARENT,
621✔
1968
                T_FALSE                  => T_FALSE,
621✔
1969
                T_TRUE                   => T_TRUE,
621✔
1970
                T_NULL                   => T_NULL,
621✔
1971
                T_NAMESPACE              => T_NAMESPACE,
621✔
1972
                T_NS_SEPARATOR           => T_NS_SEPARATOR,
621✔
1973
                T_TYPE_UNION             => T_TYPE_UNION,
621✔
1974
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
621✔
1975
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
621✔
1976
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
621✔
1977
            ];
414✔
1978

1979
            for ($i; $i < $stackPtr; $i++) {
621✔
1980
                if ($this->tokens[$i]['code'] === T_VARIABLE) {
621✔
1981
                    // Hit another variable in a group definition.
1982
                    break;
90✔
1983
                }
1984

1985
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
549✔
1986
                    $nullableType = true;
153✔
1987
                }
51✔
1988

1989
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
549✔
1990
                    $typeEndToken = $i;
549✔
1991
                    if ($typeToken === false) {
549✔
1992
                        $typeToken = $i;
549✔
1993
                    }
183✔
1994

1995
                    $type .= $this->tokens[$i]['content'];
549✔
1996
                }
183✔
1997
            }
183✔
1998

1999
            if ($type !== '' && $nullableType === true) {
621✔
2000
                $type = '?'.$type;
153✔
2001
            }
51✔
2002
        }//end if
207✔
2003

2004
        return [
270✔
2005
            'scope'           => $scope,
810✔
2006
            'scope_specified' => $scopeSpecified,
810✔
2007
            'is_static'       => $isStatic,
810✔
2008
            'is_readonly'     => $isReadonly,
810✔
2009
            'is_final'        => $isFinal,
810✔
2010
            'type'            => $type,
810✔
2011
            'type_token'      => $typeToken,
810✔
2012
            'type_end_token'  => $typeEndToken,
810✔
2013
            'nullable_type'   => $nullableType,
810✔
2014
        ];
540✔
2015

2016
    }//end getMemberProperties()
2017

2018

2019
    /**
2020
     * Returns the visibility and implementation properties of a class.
2021
     *
2022
     * The format of the return value is:
2023
     * <code>
2024
     *   array(
2025
     *    'is_abstract' => boolean, // TRUE if the abstract keyword was found.
2026
     *    'is_final'    => boolean, // TRUE if the final keyword was found.
2027
     *    'is_readonly' => boolean, // TRUE if the readonly keyword was found.
2028
     *   );
2029
     * </code>
2030
     *
2031
     * @param int $stackPtr The position in the stack of the T_CLASS token to
2032
     *                      acquire the properties for.
2033
     *
2034
     * @return array
2035
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
2036
     *                                                      T_CLASS token.
2037
     */
2038
    public function getClassProperties($stackPtr)
126✔
2039
    {
2040
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS) {
126✔
2041
            throw new RuntimeException('$stackPtr must be of type T_CLASS');
27✔
2042
        }
2043

2044
        $valid = [
33✔
2045
            T_FINAL       => T_FINAL,
99✔
2046
            T_ABSTRACT    => T_ABSTRACT,
99✔
2047
            T_READONLY    => T_READONLY,
99✔
2048
            T_WHITESPACE  => T_WHITESPACE,
99✔
2049
            T_COMMENT     => T_COMMENT,
99✔
2050
            T_DOC_COMMENT => T_DOC_COMMENT,
99✔
2051
        ];
66✔
2052

2053
        $isAbstract = false;
99✔
2054
        $isFinal    = false;
99✔
2055
        $isReadonly = false;
99✔
2056

2057
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
99✔
2058
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
99✔
2059
                break;
99✔
2060
            }
2061

2062
            switch ($this->tokens[$i]['code']) {
99✔
2063
            case T_ABSTRACT:
99✔
2064
                $isAbstract = true;
45✔
2065
                break;
45✔
2066

2067
            case T_FINAL:
99✔
2068
                $isFinal = true;
36✔
2069
                break;
36✔
2070

2071
            case T_READONLY:
99✔
2072
                $isReadonly = true;
45✔
2073
                break;
45✔
2074
            }
33✔
2075
        }//end for
33✔
2076

2077
        return [
33✔
2078
            'is_abstract' => $isAbstract,
99✔
2079
            'is_final'    => $isFinal,
99✔
2080
            'is_readonly' => $isReadonly,
99✔
2081
        ];
66✔
2082

2083
    }//end getClassProperties()
2084

2085

2086
    /**
2087
     * Determine if the passed token is a reference operator.
2088
     *
2089
     * Returns true if the specified token position represents a reference.
2090
     * Returns false if the token represents a bitwise operator.
2091
     *
2092
     * @param int $stackPtr The position of the T_BITWISE_AND token.
2093
     *
2094
     * @return boolean
2095
     */
2096
    public function isReference($stackPtr)
684✔
2097
    {
2098
        if ($this->tokens[$stackPtr]['code'] !== T_BITWISE_AND) {
684✔
2099
            return false;
27✔
2100
        }
2101

2102
        $tokenBefore = $this->findPrevious(
657✔
2103
            Tokens::$emptyTokens,
657✔
2104
            ($stackPtr - 1),
657✔
2105
            null,
657✔
2106
            true
438✔
2107
        );
438✔
2108

2109
        if ($this->tokens[$tokenBefore]['code'] === T_FUNCTION
657✔
2110
            || $this->tokens[$tokenBefore]['code'] === T_CLOSURE
651✔
2111
            || $this->tokens[$tokenBefore]['code'] === T_FN
654✔
2112
        ) {
219✔
2113
            // Function returns a reference.
2114
            return true;
27✔
2115
        }
2116

2117
        if ($this->tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
630✔
2118
            // Inside a foreach loop or array assignment, this is a reference.
2119
            return true;
54✔
2120
        }
2121

2122
        if ($this->tokens[$tokenBefore]['code'] === T_AS) {
576✔
2123
            // Inside a foreach loop, this is a reference.
2124
            return true;
9✔
2125
        }
2126

2127
        if (isset(Tokens::$assignmentTokens[$this->tokens[$tokenBefore]['code']]) === true) {
567✔
2128
            // This is directly after an assignment. It's a reference. Even if
2129
            // it is part of an operation, the other tests will handle it.
2130
            return true;
63✔
2131
        }
2132

2133
        $tokenAfter = $this->findNext(
504✔
2134
            Tokens::$emptyTokens,
504✔
2135
            ($stackPtr + 1),
504✔
2136
            null,
504✔
2137
            true
336✔
2138
        );
336✔
2139

2140
        if ($this->tokens[$tokenAfter]['code'] === T_NEW) {
504✔
2141
            return true;
9✔
2142
        }
2143

2144
        if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === true) {
495✔
2145
            $brackets    = $this->tokens[$stackPtr]['nested_parenthesis'];
351✔
2146
            $lastBracket = array_pop($brackets);
351✔
2147
            if (isset($this->tokens[$lastBracket]['parenthesis_owner']) === true) {
351✔
2148
                $owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']];
216✔
2149
                if ($owner['code'] === T_FUNCTION
216✔
2150
                    || $owner['code'] === T_CLOSURE
192✔
2151
                    || $owner['code'] === T_FN
204✔
2152
                ) {
72✔
2153
                    $params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']);
153✔
2154
                    foreach ($params as $param) {
174✔
2155
                        if ($param['reference_token'] === $stackPtr) {
153✔
2156
                            // Function parameter declared to be passed by reference.
2157
                            return true;
108✔
2158
                        }
2159
                    }
33✔
2160
                }//end if
15✔
2161
            } else {
36✔
2162
                $prev = false;
135✔
2163
                for ($t = ($this->tokens[$lastBracket]['parenthesis_opener'] - 1); $t >= 0; $t--) {
135✔
2164
                    if ($this->tokens[$t]['code'] !== T_WHITESPACE) {
135✔
2165
                        $prev = $t;
135✔
2166
                        break;
135✔
2167
                    }
2168
                }
9✔
2169

2170
                if ($prev !== false && $this->tokens[$prev]['code'] === T_USE) {
135✔
2171
                    // Closure use by reference.
2172
                    return true;
9✔
2173
                }
2174
            }//end if
2175
        }//end if
78✔
2176

2177
        // Pass by reference in function calls and assign by reference in arrays.
2178
        if ($this->tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
378✔
2179
            || $this->tokens[$tokenBefore]['code'] === T_COMMA
348✔
2180
            || $this->tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY
363✔
2181
        ) {
126✔
2182
            if ($this->tokens[$tokenAfter]['code'] === T_VARIABLE) {
270✔
2183
                return true;
198✔
2184
            } else {
2185
                $skip   = Tokens::$emptyTokens;
72✔
2186
                $skip[] = T_NS_SEPARATOR;
72✔
2187
                $skip[] = T_SELF;
72✔
2188
                $skip[] = T_PARENT;
72✔
2189
                $skip[] = T_STATIC;
72✔
2190
                $skip[] = T_STRING;
72✔
2191
                $skip[] = T_NAMESPACE;
72✔
2192
                $skip[] = T_DOUBLE_COLON;
72✔
2193

2194
                $nextSignificantAfter = $this->findNext(
72✔
2195
                    $skip,
72✔
2196
                    ($stackPtr + 1),
72✔
2197
                    null,
72✔
2198
                    true
48✔
2199
                );
48✔
2200
                if ($this->tokens[$nextSignificantAfter]['code'] === T_VARIABLE) {
72✔
2201
                    return true;
72✔
2202
                }
2203
            }//end if
2204
        }//end if
2205

2206
        return false;
108✔
2207

2208
    }//end isReference()
2209

2210

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

2229
        if (is_int($length) === false || $length <= 0) {
234✔
2230
            return '';
27✔
2231
        }
2232

2233
        $str = '';
207✔
2234
        $end = ($start + $length);
207✔
2235
        if ($end > $this->numTokens) {
207✔
2236
            $end = $this->numTokens;
9✔
2237
        }
3✔
2238

2239
        for ($i = $start; $i < $end; $i++) {
207✔
2240
            // If tabs are being converted to spaces by the tokeniser, the
2241
            // original content should be used instead of the converted content.
2242
            if ($origContent === true && isset($this->tokens[$i]['orig_content']) === true) {
207✔
2243
                $str .= $this->tokens[$i]['orig_content'];
18✔
2244
            } else {
6✔
2245
                $str .= $this->tokens[$i]['content'];
207✔
2246
            }
2247
        }
69✔
2248

2249
        return $str;
207✔
2250

2251
    }//end getTokensAsString()
2252

2253

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

2290
        if ($end === null) {
×
2291
            $end = 0;
×
2292
        }
2293

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

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

2311
            if ($local === true) {
×
2312
                if (isset($this->tokens[$i]['scope_opener']) === true
×
2313
                    && $i === $this->tokens[$i]['scope_closer']
×
2314
                ) {
2315
                    $i = $this->tokens[$i]['scope_opener'];
×
2316
                } else if (isset($this->tokens[$i]['bracket_opener']) === true
×
2317
                    && $i === $this->tokens[$i]['bracket_closer']
×
2318
                ) {
2319
                    $i = $this->tokens[$i]['bracket_opener'];
×
2320
                } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
×
2321
                    && $i === $this->tokens[$i]['parenthesis_closer']
×
2322
                ) {
2323
                    $i = $this->tokens[$i]['parenthesis_opener'];
×
2324
                } else if ($this->tokens[$i]['code'] === T_SEMICOLON) {
×
2325
                    break;
×
2326
                }
2327
            }
2328
        }//end for
2329

2330
        return false;
×
2331

2332
    }//end findPrevious()
2333

2334

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

2371
        if ($end === null || $end > $this->numTokens) {
×
2372
            $end = $this->numTokens;
×
2373
        }
2374

2375
        for ($i = $start; $i < $end; $i++) {
×
2376
            $found = (bool) $exclude;
×
2377
            foreach ($types as $type) {
×
2378
                if ($this->tokens[$i]['code'] === $type) {
×
2379
                    $found = !$exclude;
×
2380
                    break;
×
2381
                }
2382
            }
2383

2384
            if ($found === true) {
×
2385
                if ($value === null) {
×
2386
                    return $i;
×
2387
                } else if ($this->tokens[$i]['content'] === $value) {
×
2388
                    return $i;
×
2389
                }
2390
            }
2391

2392
            if ($local === true && $this->tokens[$i]['code'] === T_SEMICOLON) {
×
2393
                break;
×
2394
            }
2395
        }//end for
2396

2397
        return false;
×
2398

2399
    }//end findNext()
2400

2401

2402
    /**
2403
     * Returns the position of the first non-whitespace token in a statement.
2404
     *
2405
     * @param int              $start  The position to start searching from in the token stack.
2406
     * @param int|string|array $ignore Token types that should not be considered stop points.
2407
     *
2408
     * @return int
2409
     */
2410
    public function findStartOfStatement($start, $ignore=null)
630✔
2411
    {
2412
        $startTokens = Tokens::$blockOpeners;
630✔
2413
        $startTokens[T_OPEN_SHORT_ARRAY]   = true;
630✔
2414
        $startTokens[T_OPEN_TAG]           = true;
630✔
2415
        $startTokens[T_OPEN_TAG_WITH_ECHO] = true;
630✔
2416

2417
        $endTokens = [
210✔
2418
            T_CLOSE_TAG    => true,
630✔
2419
            T_COLON        => true,
630✔
2420
            T_COMMA        => true,
630✔
2421
            T_DOUBLE_ARROW => true,
630✔
2422
            T_MATCH_ARROW  => true,
630✔
2423
            T_SEMICOLON    => true,
630✔
2424
        ];
420✔
2425

2426
        if ($ignore !== null) {
630✔
2427
            $ignore = (array) $ignore;
×
2428
            foreach ($ignore as $code) {
×
2429
                if (isset($startTokens[$code]) === true) {
×
2430
                    unset($startTokens[$code]);
×
2431
                }
2432

2433
                if (isset($endTokens[$code]) === true) {
×
2434
                    unset($endTokens[$code]);
×
2435
                }
2436
            }
2437
        }
2438

2439
        // If the start token is inside the case part of a match expression,
2440
        // find the start of the condition. If it's in the statement part, find
2441
        // the token that comes after the match arrow.
2442
        if (empty($this->tokens[$start]['conditions']) === false) {
630✔
2443
            $conditions         = $this->tokens[$start]['conditions'];
486✔
2444
            $lastConditionOwner = end($conditions);
486✔
2445
            $matchExpression    = key($conditions);
486✔
2446

2447
            if ($lastConditionOwner === T_MATCH
324✔
2448
                // Check if the $start token is at the same parentheses nesting level as the match token.
2449
                && ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === true
417✔
2450
                && empty($this->tokens[$start]['nested_parenthesis']) === true)
324✔
2451
                || ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === false
264✔
2452
                && empty($this->tokens[$start]['nested_parenthesis']) === false)
264✔
2453
                && $this->tokens[$matchExpression]['nested_parenthesis'] === $this->tokens[$start]['nested_parenthesis']))
375✔
2454
            ) {
162✔
2455
                // Walk back to the previous match arrow (if it exists).
2456
                $lastComma          = null;
135✔
2457
                $inNestedExpression = false;
135✔
2458
                for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
135✔
2459
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_MATCH_ARROW) {
135✔
2460
                        break;
99✔
2461
                    }
2462

2463
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_COMMA) {
135✔
2464
                        $lastComma = $prevMatch;
72✔
2465
                        continue;
72✔
2466
                    }
2467

2468
                    // Skip nested statements.
2469
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
135✔
2470
                        && $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
135✔
2471
                    ) {
45✔
2472
                        $prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
36✔
2473
                        continue;
36✔
2474
                    }
2475

2476
                    if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
135✔
2477
                        && $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
135✔
2478
                    ) {
45✔
2479
                        $prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
45✔
2480
                        continue;
45✔
2481
                    }
2482

2483
                    // Stop if we're _within_ a nested short array statement, which may contain comma's too.
2484
                    // No need to deal with parentheses, those are handled above via the `nested_parenthesis` checks.
2485
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
135✔
2486
                        && $this->tokens[$prevMatch]['bracket_closer'] > $start
135✔
2487
                    ) {
45✔
2488
                        $inNestedExpression = true;
45✔
2489
                        break;
45✔
2490
                    }
2491
                }//end for
42✔
2492

2493
                if ($inNestedExpression === false) {
135✔
2494
                    // $prevMatch will now either be the scope opener or a match arrow.
2495
                    // If it is the scope opener, go the first non-empty token after. $start will have been part of the first condition.
2496
                    if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) {
99✔
2497
                        // We're before the arrow in the first case.
2498
                        $next = $this->findNext(Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
36✔
2499
                        if ($next === false) {
36✔
2500
                            // Shouldn't be possible.
2501
                            return $start;
×
2502
                        }
2503

2504
                        return $next;
36✔
2505
                    }
2506

2507
                    // Okay, so we found a match arrow.
2508
                    // If $start was part of the "next" condition, the last comma will be set.
2509
                    // Otherwise, $start must have been part of a return expression.
2510
                    if (isset($lastComma) === true && $lastComma > $prevMatch) {
99✔
2511
                        $prevMatch = $lastComma;
45✔
2512
                    }
15✔
2513

2514
                    // In both cases, go to the first non-empty token after.
2515
                    $next = $this->findNext(Tokens::$emptyTokens, ($prevMatch + 1), null, true);
99✔
2516
                    if ($next === false) {
99✔
2517
                        // Shouldn't be possible.
2518
                        return $start;
×
2519
                    }
2520

2521
                    return $next;
99✔
2522
                }//end if
2523
            }//end if
15✔
2524
        }//end if
132✔
2525

2526
        $lastNotEmpty = $start;
540✔
2527

2528
        // If we are starting at a token that ends a scope block, skip to
2529
        // the start and continue from there.
2530
        // If we are starting at a token that ends a statement, skip this
2531
        // token so we find the true start of the statement.
2532
        while (isset($endTokens[$this->tokens[$start]['code']]) === true
540✔
2533
            || (isset($this->tokens[$start]['scope_condition']) === true
540✔
2534
            && $start === $this->tokens[$start]['scope_closer'])
540✔
2535
        ) {
180✔
2536
            if (isset($this->tokens[$start]['scope_condition']) === true) {
153✔
2537
                $start = $this->tokens[$start]['scope_condition'];
81✔
2538
            } else {
27✔
2539
                $start--;
90✔
2540
            }
2541
        }
51✔
2542

2543
        for ($i = $start; $i >= 0; $i--) {
540✔
2544
            if (isset($startTokens[$this->tokens[$i]['code']]) === true
540✔
2545
                || isset($endTokens[$this->tokens[$i]['code']]) === true
540✔
2546
            ) {
180✔
2547
                // Found the end of the previous statement.
2548
                return $lastNotEmpty;
540✔
2549
            }
2550

2551
            if (isset($this->tokens[$i]['scope_opener']) === true
531✔
2552
                && $i === $this->tokens[$i]['scope_closer']
531✔
2553
                && $this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS
531✔
2554
                && $this->tokens[$i]['code'] !== T_END_NOWDOC
531✔
2555
                && $this->tokens[$i]['code'] !== T_END_HEREDOC
531✔
2556
                && $this->tokens[$i]['code'] !== T_BREAK
531✔
2557
                && $this->tokens[$i]['code'] !== T_RETURN
531✔
2558
                && $this->tokens[$i]['code'] !== T_CONTINUE
531✔
2559
                && $this->tokens[$i]['code'] !== T_THROW
531✔
2560
                && $this->tokens[$i]['code'] !== T_EXIT
531✔
2561
                && $this->tokens[$i]['code'] !== T_GOTO
531✔
2562
            ) {
177✔
2563
                // Found the end of the previous scope block.
2564
                return $lastNotEmpty;
9✔
2565
            }
2566

2567
            // Skip nested statements.
2568
            if (isset($this->tokens[$i]['bracket_opener']) === true
531✔
2569
                && $i === $this->tokens[$i]['bracket_closer']
531✔
2570
            ) {
177✔
2571
                $i = $this->tokens[$i]['bracket_opener'];
9✔
2572
            } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
531✔
2573
                && $i === $this->tokens[$i]['parenthesis_closer']
531✔
2574
            ) {
177✔
2575
                $i = $this->tokens[$i]['parenthesis_opener'];
63✔
2576
            } else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
531✔
2577
                $start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
18✔
2578
                if ($start !== false) {
18✔
2579
                    $i = $start;
18✔
2580
                }
6✔
2581
            }//end if
6✔
2582

2583
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
531✔
2584
                $lastNotEmpty = $i;
531✔
2585
            }
177✔
2586
        }//end for
177✔
2587

2588
        return 0;
×
2589

2590
    }//end findStartOfStatement()
2591

2592

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

2616
        if ($ignore !== null) {
198✔
2617
            $ignore = (array) $ignore;
×
2618
            foreach ($ignore as $code) {
×
2619
                unset($endTokens[$code]);
×
2620
            }
2621
        }
2622

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

2641
                if ($beforeArrow === true) {
90✔
2642
                    $nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
90✔
2643
                    if ($nextMatchArrow !== false) {
90✔
2644
                        $start = $nextMatchArrow;
90✔
2645
                    }
30✔
2646
                }
30✔
2647
            }//end if
30✔
2648
        }//end if
66✔
2649

2650
        $lastNotEmpty = $start;
198✔
2651
        for ($i = $start; $i < $this->numTokens; $i++) {
198✔
2652
            if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
198✔
2653
                // Found the end of the statement.
2654
                if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS
180✔
2655
                    || $this->tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
174✔
2656
                    || $this->tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
171✔
2657
                    || $this->tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
159✔
2658
                    || $this->tokens[$i]['code'] === T_OPEN_TAG
147✔
2659
                    || $this->tokens[$i]['code'] === T_CLOSE_TAG
168✔
2660
                ) {
60✔
2661
                    return $lastNotEmpty;
72✔
2662
                }
2663

2664
                return $i;
144✔
2665
            }
2666

2667
            // Skip nested statements.
2668
            if (isset($this->tokens[$i]['scope_closer']) === true
198✔
2669
                && ($i === $this->tokens[$i]['scope_opener']
168✔
2670
                || $i === $this->tokens[$i]['scope_condition'])
168✔
2671
            ) {
66✔
2672
                if ($this->tokens[$i]['code'] === T_FN) {
108✔
2673
                    $lastNotEmpty = $this->tokens[$i]['scope_closer'];
54✔
2674
                    $i            = ($this->tokens[$i]['scope_closer'] - 1);
54✔
2675
                    continue;
54✔
2676
                }
2677

2678
                if ($i === $start && isset(Tokens::$scopeOpeners[$this->tokens[$i]['code']]) === true) {
63✔
2679
                    return $this->tokens[$i]['scope_closer'];
27✔
2680
                }
2681

2682
                $i = $this->tokens[$i]['scope_closer'];
45✔
2683
            } else if (isset($this->tokens[$i]['bracket_closer']) === true
144✔
2684
                && $i === $this->tokens[$i]['bracket_opener']
144✔
2685
            ) {
48✔
2686
                $i = $this->tokens[$i]['bracket_closer'];
18✔
2687
            } else if (isset($this->tokens[$i]['parenthesis_closer']) === true
144✔
2688
                && $i === $this->tokens[$i]['parenthesis_opener']
144✔
2689
            ) {
48✔
2690
                $i = $this->tokens[$i]['parenthesis_closer'];
27✔
2691
            } else if ($this->tokens[$i]['code'] === T_OPEN_USE_GROUP) {
144✔
2692
                $end = $this->findNext(T_CLOSE_USE_GROUP, ($i + 1));
18✔
2693
                if ($end !== false) {
18✔
2694
                    $i = $end;
18✔
2695
                }
6✔
2696
            }//end if
6✔
2697

2698
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
144✔
2699
                $lastNotEmpty = $i;
144✔
2700
            }
48✔
2701
        }//end for
48✔
2702

2703
        return ($this->numTokens - 1);
9✔
2704

2705
    }//end findEndOfStatement()
2706

2707

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

2734
        $foundToken = false;
×
2735

2736
        for ($i = $start; $i >= 0; $i--) {
×
2737
            if ($this->tokens[$i]['line'] < $this->tokens[$start]['line']) {
×
2738
                break;
×
2739
            }
2740

2741
            $found = $exclude;
×
2742
            foreach ($types as $type) {
×
2743
                if ($exclude === false) {
×
2744
                    if ($this->tokens[$i]['code'] === $type) {
×
2745
                        $found = true;
×
2746
                        break;
×
2747
                    }
2748
                } else {
2749
                    if ($this->tokens[$i]['code'] === $type) {
×
2750
                        $found = false;
×
2751
                        break;
×
2752
                    }
2753
                }
2754
            }
2755

2756
            if ($found === true) {
×
2757
                if ($value === null) {
×
2758
                    $foundToken = $i;
×
2759
                } else if ($this->tokens[$i]['content'] === $value) {
×
2760
                    $foundToken = $i;
×
2761
                }
2762
            }
2763
        }//end for
2764

2765
        return $foundToken;
×
2766

2767
    }//end findFirstOnLine()
2768

2769

2770
    /**
2771
     * Determine if the passed token has a condition of one of the passed types.
2772
     *
2773
     * @param int              $stackPtr The position of the token we are checking.
2774
     * @param int|string|array $types    The type(s) of tokens to search for.
2775
     *
2776
     * @return boolean
2777
     */
2778
    public function hasCondition($stackPtr, $types)
63✔
2779
    {
2780
        // Check for the existence of the token.
2781
        if (isset($this->tokens[$stackPtr]) === false) {
63✔
2782
            return false;
9✔
2783
        }
2784

2785
        // Make sure the token has conditions.
2786
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
54✔
2787
            return false;
9✔
2788
        }
2789

2790
        $types      = (array) $types;
45✔
2791
        $conditions = $this->tokens[$stackPtr]['conditions'];
45✔
2792

2793
        foreach ($types as $type) {
45✔
2794
            if (in_array($type, $conditions, true) === true) {
45✔
2795
                // We found a token with the required type.
2796
                return true;
45✔
2797
            }
2798
        }
15✔
2799

2800
        return false;
45✔
2801

2802
    }//end hasCondition()
2803

2804

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

2826
        // Make sure the token has conditions.
2827
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
81✔
2828
            return false;
9✔
2829
        }
2830

2831
        $conditions = $this->tokens[$stackPtr]['conditions'];
72✔
2832
        if ($first === false) {
72✔
2833
            $conditions = array_reverse($conditions, true);
36✔
2834
        }
12✔
2835

2836
        foreach ($conditions as $token => $condition) {
72✔
2837
            if ($condition === $type) {
72✔
2838
                return $token;
72✔
2839
            }
2840
        }
24✔
2841

2842
        return false;
72✔
2843

2844
    }//end getCondition()
2845

2846

2847
    /**
2848
     * Returns the name of the class that the specified class extends.
2849
     * (works for classes, anonymous classes and interfaces)
2850
     *
2851
     * Returns FALSE on error or if there is no extended class name.
2852
     *
2853
     * @param int $stackPtr The stack position of the class.
2854
     *
2855
     * @return string|false
2856
     */
2857
    public function findExtendedClassName($stackPtr)
153✔
2858
    {
2859
        // Check for the existence of the token.
2860
        if (isset($this->tokens[$stackPtr]) === false) {
153✔
2861
            return false;
9✔
2862
        }
2863

2864
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
144✔
2865
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
144✔
2866
            && $this->tokens[$stackPtr]['code'] !== T_INTERFACE
144✔
2867
        ) {
48✔
2868
            return false;
9✔
2869
        }
2870

2871
        if (isset($this->tokens[$stackPtr]['scope_opener']) === false) {
135✔
2872
            return false;
9✔
2873
        }
2874

2875
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
126✔
2876
        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex);
126✔
2877
        if ($extendsIndex === false) {
126✔
2878
            return false;
27✔
2879
        }
2880

2881
        $find = [
33✔
2882
            T_NS_SEPARATOR,
99✔
2883
            T_STRING,
99✔
2884
            T_WHITESPACE,
99✔
2885
        ];
66✔
2886

2887
        $end  = $this->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true);
99✔
2888
        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
99✔
2889
        $name = trim($name);
99✔
2890

2891
        if ($name === '') {
99✔
2892
            return false;
9✔
2893
        }
2894

2895
        return $name;
90✔
2896

2897
    }//end findExtendedClassName()
2898

2899

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

2916
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
135✔
2917
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
135✔
2918
            && $this->tokens[$stackPtr]['code'] !== T_ENUM
135✔
2919
        ) {
45✔
2920
            return false;
18✔
2921
        }
2922

2923
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
117✔
2924
            return false;
9✔
2925
        }
2926

2927
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
108✔
2928
        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
108✔
2929
        if ($implementsIndex === false) {
108✔
2930
            return false;
18✔
2931
        }
2932

2933
        $find = [
30✔
2934
            T_NS_SEPARATOR,
90✔
2935
            T_STRING,
90✔
2936
            T_WHITESPACE,
90✔
2937
            T_COMMA,
90✔
2938
        ];
60✔
2939

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

2944
        if ($name === '') {
90✔
2945
            return false;
9✔
2946
        } else {
2947
            $names = explode(',', $name);
81✔
2948
            $names = array_map('trim', $names);
81✔
2949
            return $names;
81✔
2950
        }
2951

2952
    }//end findImplementedInterfaceNames()
2953

2954

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

© 2025 Coveralls, Inc