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

PHPCSStandards / PHP_CodeSniffer / 13506314473

24 Feb 2025 07:34PM UTC coverage: 78.585% (+0.1%) from 78.468%
13506314473

Pull #812

github

web-flow
Merge 6ed9ac8b2 into bf2b64d00
Pull Request #812: Tests/Tokenizer: improve switch keyword tests

24737 of 31478 relevant lines covered (78.59%)

66.37 hits per line

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

71.56
/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
        $annotationErrors = [];
×
350

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

432
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
433
                $type    = $token['type'];
×
434
                $content = Common::prepareForOutput($token['content']);
×
435
                echo "\t\tProcess token $stackPtr: $type => $content".PHP_EOL;
×
436
            }
437

438
            if ($token['code'] !== T_INLINE_HTML) {
×
439
                $foundCode = true;
×
440
            }
441

442
            if (isset($this->ruleset->tokenListeners[$token['code']]) === false) {
×
443
                continue;
×
444
            }
445

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

455
                // Make sure this sniff supports the tokenizer
456
                // we are currently using.
457
                $class = $listenerData['class'];
×
458

459
                if (isset($listenerData['tokenizers'][$this->tokenizerType]) === false) {
×
460
                    continue;
×
461
                }
462

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

475
                        $pattern = '`'.$pattern.'`i';
×
476
                        if (preg_match($pattern, $this->path) === 1) {
×
477
                            $this->ignoredListeners[$class] = true;
×
478
                            continue(2);
×
479
                        }
480
                    }
481

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

495
                            $pattern = '`'.$pattern.'`i';
×
496
                            if (preg_match($pattern, $this->path) === 1) {
×
497
                                $included = true;
×
498
                                break;
×
499
                            }
500
                        }
501

502
                        if ($included === false) {
×
503
                            $this->ignoredListeners[$class] = true;
×
504
                            continue;
×
505
                        }
506
                    }//end if
507
                }//end if
508

509
                $this->activeListener = $class;
×
510

511
                if ($this->configCache['trackTime'] === true) {
×
512
                    $startTime = microtime(true);
×
513
                }
514

515
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
516
                    echo "\t\t\tProcessing ".$this->activeListener.'... ';
×
517
                }
518

519
                $ignoreTo = $this->ruleset->sniffs[$class]->process($this, $stackPtr);
×
520
                if ($ignoreTo !== null) {
×
521
                    $listenerIgnoreTo[$this->activeListener] = $ignoreTo;
×
522
                }
523

524
                if ($this->configCache['trackTime'] === true) {
×
525
                    $timeTaken = (microtime(true) - $startTime);
×
526
                    if (isset($this->listenerTimes[$this->activeListener]) === false) {
×
527
                        $this->listenerTimes[$this->activeListener] = 0;
×
528
                    }
529

530
                    $this->listenerTimes[$this->activeListener] += $timeTaken;
×
531
                }
532

533
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
534
                    $timeTaken = round(($timeTaken), 4);
×
535
                    echo "DONE in $timeTaken seconds".PHP_EOL;
×
536
                }
537

538
                $this->activeListener = '';
×
539
            }//end foreach
540
        }//end foreach
541

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

556
        if ($annotationErrors !== []) {
×
557
            $error  = 'Encountered invalid inline phpcs:set annotations. Found:'.PHP_EOL;
×
558
            $error .= implode(PHP_EOL, $annotationErrors);
×
559

560
            $this->addWarning($error, null, 'Internal.PropertyDoesNotExist');
×
561
        }
562

563
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
564
            echo "\t*** END TOKEN PROCESSING ***".PHP_EOL;
×
565
            echo "\t*** START SNIFF PROCESSING REPORT ***".PHP_EOL;
×
566

567
            arsort($this->listenerTimes, SORT_NUMERIC);
×
568
            foreach ($this->listenerTimes as $listener => $timeTaken) {
×
569
                echo "\t$listener: ".round(($timeTaken), 4).' secs'.PHP_EOL;
×
570
            }
571

572
            echo "\t*** END SNIFF PROCESSING REPORT ***".PHP_EOL;
×
573
        }
574

575
        $this->fixedCount += $this->fixer->getFixCount();
×
576

577
    }//end process()
578

579

580
    /**
581
     * Tokenizes the file and prepares it for the test run.
582
     *
583
     * @return void
584
     */
585
    public function parse()
×
586
    {
587
        if (empty($this->tokens) === false) {
×
588
            // File has already been parsed.
589
            return;
×
590
        }
591

592
        try {
593
            $tokenizerClass  = 'PHP_CodeSniffer\Tokenizers\\'.$this->tokenizerType;
×
594
            $this->tokenizer = new $tokenizerClass($this->content, $this->config, $this->eolChar);
×
595
            $this->tokens    = $this->tokenizer->getTokens();
×
596
        } catch (TokenizerException $e) {
×
597
            $this->ignored = true;
×
598
            $this->addWarning($e->getMessage(), null, 'Internal.Tokenizer.Exception');
×
599
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
600
                echo "[$this->tokenizerType => tokenizer error]... ";
×
601
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
602
                    echo PHP_EOL;
×
603
                }
604
            }
605

606
            return;
×
607
        }
608

609
        $this->numTokens = count($this->tokens);
×
610

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

625
        if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
626
            if ($this->numTokens === 0) {
×
627
                $numLines = 0;
×
628
            } else {
629
                $numLines = $this->tokens[($this->numTokens - 1)]['line'];
×
630
            }
631

632
            echo "[$this->tokenizerType => $this->numTokens tokens in $numLines lines]... ";
×
633
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
634
                echo PHP_EOL;
×
635
            }
636
        }
637

638
    }//end parse()
639

640

641
    /**
642
     * Returns the token stack for this file.
643
     *
644
     * @return array
645
     */
646
    public function getTokens()
×
647
    {
648
        return $this->tokens;
×
649

650
    }//end getTokens()
651

652

653
    /**
654
     * Remove vars stored in this file that are no longer required.
655
     *
656
     * @return void
657
     */
658
    public function cleanUp()
×
659
    {
660
        $this->listenerTimes = null;
×
661
        $this->content       = null;
×
662
        $this->tokens        = null;
×
663
        $this->metricTokens  = null;
×
664
        $this->tokenizer     = null;
×
665
        $this->fixer         = null;
×
666
        $this->config        = null;
×
667
        $this->ruleset       = null;
×
668

669
    }//end cleanUp()
670

671

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

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

703
    }//end addError()
704

705

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

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

737
    }//end addWarning()
738

739

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

761
    }//end addErrorOnLine()
762

763

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

785
    }//end addWarningOnLine()
786

787

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

814
        return false;
×
815

816
    }//end addFixableError()
817

818

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

845
        return false;
×
846

847
    }//end addFixableWarning()
848

849

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

872
        // Work out which sniff generated the message.
873
        $parts = explode('.', $code);
183✔
874
        if ($parts[0] === 'Internal') {
183✔
875
            // An internal message.
876
            $listenerCode = '';
×
877
            if ($this->activeListener !== '') {
×
878
                $listenerCode = Common::getSniffCode($this->activeListener);
×
879
            }
880

881
            $sniffCode  = $code;
×
882
            $checkCodes = [$sniffCode];
×
883
        } else {
884
            if ($parts[0] !== $code) {
183✔
885
                // The full message code has been passed in.
886
                $sniffCode    = $code;
×
887
                $listenerCode = substr($sniffCode, 0, strrpos($sniffCode, '.'));
×
888
            } else {
889
                $listenerCode = Common::getSniffCode($this->activeListener);
183✔
890
                $sniffCode    = $listenerCode.'.'.$code;
183✔
891
                $parts        = explode('.', $sniffCode);
183✔
892
            }
893

894
            $checkCodes = [
61✔
895
                $sniffCode,
183✔
896
                $parts[0].'.'.$parts[1].'.'.$parts[2],
183✔
897
                $parts[0].'.'.$parts[1],
183✔
898
                $parts[0],
183✔
899
            ];
122✔
900
        }//end if
901

902
        if (isset($this->tokenizer->ignoredLines[$line]) === true) {
183✔
903
            // Check if this line is ignoring this specific message.
904
            $ignored = false;
99✔
905
            foreach ($checkCodes as $checkCode) {
99✔
906
                if (isset($this->tokenizer->ignoredLines[$line][$checkCode]) === true) {
99✔
907
                    $ignored = true;
90✔
908
                    break;
90✔
909
                }
910
            }
33✔
911

912
            // If it is ignored, make sure there is no exception in place.
913
            if ($ignored === true
66✔
914
                && isset($this->tokenizer->ignoredLines[$line]['.except']) === true
99✔
915
            ) {
33✔
916
                foreach ($checkCodes as $checkCode) {
15✔
917
                    if (isset($this->tokenizer->ignoredLines[$line]['.except'][$checkCode]) === true) {
15✔
918
                        $ignored = false;
12✔
919
                        break;
12✔
920
                    }
921
                }
5✔
922
            }
5✔
923

924
            if ($ignored === true) {
99✔
925
                return false;
90✔
926
            }
927
        }//end if
25✔
928

929
        $includeAll = true;
171✔
930
        if ($this->configCache['cache'] === false
171✔
931
            || $this->configCache['recordErrors'] === false
171✔
932
        ) {
57✔
933
            $includeAll = false;
171✔
934
        }
57✔
935

936
        // Filter out any messages for sniffs that shouldn't have run
937
        // due to the use of the --sniffs command line argument.
938
        if ($includeAll === false
114✔
939
            && ((empty($this->configCache['sniffs']) === false
171✔
940
            && in_array(strtolower($listenerCode), $this->configCache['sniffs'], true) === false)
171✔
941
            || (empty($this->configCache['exclude']) === false
171✔
942
            && in_array(strtolower($listenerCode), $this->configCache['exclude'], true) === true))
171✔
943
        ) {
57✔
944
            return false;
×
945
        }
946

947
        // If we know this sniff code is being ignored for this file, return early.
948
        foreach ($checkCodes as $checkCode) {
171✔
949
            if (isset($this->ignoredCodes[$checkCode]) === true) {
171✔
950
                return false;
×
951
            }
952
        }
57✔
953

954
        $oppositeType = 'warning';
171✔
955
        if ($error === false) {
171✔
956
            $oppositeType = 'error';
78✔
957
        }
26✔
958

959
        foreach ($checkCodes as $checkCode) {
171✔
960
            // Make sure this message type has not been set to the opposite message type.
961
            if (isset($this->ruleset->ruleset[$checkCode]['type']) === true
171✔
962
                && $this->ruleset->ruleset[$checkCode]['type'] === $oppositeType
171✔
963
            ) {
57✔
964
                $error = !$error;
×
965
                break;
×
966
            }
967
        }
57✔
968

969
        if ($error === true) {
171✔
970
            $configSeverity = $this->configCache['errorSeverity'];
144✔
971
            $messageCount   = &$this->errorCount;
144✔
972
            $messages       = &$this->errors;
144✔
973
        } else {
48✔
974
            $configSeverity = $this->configCache['warningSeverity'];
78✔
975
            $messageCount   = &$this->warningCount;
78✔
976
            $messages       = &$this->warnings;
78✔
977
        }
978

979
        if ($includeAll === false && $configSeverity === 0) {
171✔
980
            // Don't bother doing any processing as these messages are just going to
981
            // be hidden in the reports anyway.
982
            return false;
×
983
        }
984

985
        if ($severity === 0) {
171✔
986
            $severity = 5;
171✔
987
        }
57✔
988

989
        foreach ($checkCodes as $checkCode) {
171✔
990
            // Make sure we are interested in this severity level.
991
            if (isset($this->ruleset->ruleset[$checkCode]['severity']) === true) {
171✔
992
                $severity = $this->ruleset->ruleset[$checkCode]['severity'];
×
993
                break;
×
994
            }
995
        }
57✔
996

997
        if ($includeAll === false && $configSeverity > $severity) {
171✔
998
            return false;
×
999
        }
1000

1001
        // Make sure we are not ignoring this file.
1002
        $included = null;
171✔
1003
        if (trim($this->path, '\'"') === 'STDIN') {
171✔
1004
            $included = true;
171✔
1005
        } else {
57✔
1006
            foreach ($checkCodes as $checkCode) {
×
1007
                $patterns = null;
×
1008

1009
                if (isset($this->configCache['includePatterns'][$checkCode]) === true) {
×
1010
                    $patterns  = $this->configCache['includePatterns'][$checkCode];
×
1011
                    $excluding = false;
×
1012
                } else if (isset($this->configCache['ignorePatterns'][$checkCode]) === true) {
×
1013
                    $patterns  = $this->configCache['ignorePatterns'][$checkCode];
×
1014
                    $excluding = true;
×
1015
                }
1016

1017
                if ($patterns === null) {
×
1018
                    continue;
×
1019
                }
1020

1021
                foreach ($patterns as $pattern => $type) {
×
1022
                    // While there is support for a type of each pattern
1023
                    // (absolute or relative) we don't actually support it here.
1024
                    $replacements = [
1025
                        '\\,' => ',',
×
1026
                        '*'   => '.*',
1027
                    ];
1028

1029
                    // We assume a / directory separator, as do the exclude rules
1030
                    // most developers write, so we need a special case for any system
1031
                    // that is different.
1032
                    if (DIRECTORY_SEPARATOR === '\\') {
×
1033
                        $replacements['/'] = '\\\\';
×
1034
                    }
1035

1036
                    $pattern = '`'.strtr($pattern, $replacements).'`i';
×
1037
                    $matched = preg_match($pattern, $this->path);
×
1038

1039
                    if ($matched === 0) {
×
1040
                        if ($excluding === false && $included === null) {
×
1041
                            // This file path is not being included.
1042
                            $included = false;
×
1043
                        }
1044

1045
                        continue;
×
1046
                    }
1047

1048
                    if ($excluding === true) {
×
1049
                        // This file path is being excluded.
1050
                        $this->ignoredCodes[$checkCode] = true;
×
1051
                        return false;
×
1052
                    }
1053

1054
                    // This file path is being included.
1055
                    $included = true;
×
1056
                    break;
×
1057
                }//end foreach
1058
            }//end foreach
1059
        }//end if
1060

1061
        if ($included === false) {
171✔
1062
            // There were include rules set, but this file
1063
            // path didn't match any of them.
1064
            return false;
×
1065
        }
1066

1067
        $messageCount++;
171✔
1068
        if ($fixable === true) {
171✔
1069
            $this->fixableCount++;
144✔
1070
        }
48✔
1071

1072
        if ($this->configCache['recordErrors'] === false
171✔
1073
            && $includeAll === false
171✔
1074
        ) {
57✔
1075
            return true;
×
1076
        }
1077

1078
        // See if there is a custom error message format to use.
1079
        // But don't do this if we are replaying errors because replayed
1080
        // errors have already used the custom format and have had their
1081
        // data replaced.
1082
        if ($this->replayingErrors === false
171✔
1083
            && isset($this->ruleset->ruleset[$sniffCode]['message']) === true
171✔
1084
        ) {
57✔
1085
            $message = $this->ruleset->ruleset[$sniffCode]['message'];
×
1086
        }
1087

1088
        if (empty($data) === false) {
171✔
1089
            $message = vsprintf($message, $data);
168✔
1090
        }
56✔
1091

1092
        if (isset($messages[$line]) === false) {
171✔
1093
            $messages[$line] = [];
171✔
1094
        }
57✔
1095

1096
        if (isset($messages[$line][$column]) === false) {
171✔
1097
            $messages[$line][$column] = [];
171✔
1098
        }
57✔
1099

1100
        $messages[$line][$column][] = [
171✔
1101
            'message'  => $message,
171✔
1102
            'source'   => $sniffCode,
171✔
1103
            'listener' => $this->activeListener,
171✔
1104
            'severity' => $severity,
171✔
1105
            'fixable'  => $fixable,
171✔
1106
        ];
57✔
1107

1108
        if (PHP_CODESNIFFER_VERBOSITY > 1
171✔
1109
            && $this->fixer->enabled === true
171✔
1110
            && $fixable === true
171✔
1111
        ) {
57✔
1112
            @ob_end_clean();
×
1113
            echo "\tE: [Line $line] $message ($sniffCode)".PHP_EOL;
×
1114
            ob_start();
×
1115
        }
1116

1117
        return true;
171✔
1118

1119
    }//end addMessage()
1120

1121

1122
    /**
1123
     * Record a metric about the file being examined.
1124
     *
1125
     * @param int    $stackPtr The stack position where the metric was recorded.
1126
     * @param string $metric   The name of the metric being recorded.
1127
     * @param string $value    The value of the metric being recorded.
1128
     *
1129
     * @return boolean
1130
     */
1131
    public function recordMetric($stackPtr, $metric, $value)
×
1132
    {
1133
        if (isset($this->metrics[$metric]) === false) {
×
1134
            $this->metrics[$metric] = ['values' => [$value => 1]];
×
1135
            $this->metricTokens[$metric][$stackPtr] = true;
×
1136
        } else if (isset($this->metricTokens[$metric][$stackPtr]) === false) {
×
1137
            $this->metricTokens[$metric][$stackPtr] = true;
×
1138
            if (isset($this->metrics[$metric]['values'][$value]) === false) {
×
1139
                $this->metrics[$metric]['values'][$value] = 1;
×
1140
            } else {
1141
                $this->metrics[$metric]['values'][$value]++;
×
1142
            }
1143
        }
1144

1145
        return true;
×
1146

1147
    }//end recordMetric()
1148

1149

1150
    /**
1151
     * Returns the number of errors raised.
1152
     *
1153
     * @return int
1154
     */
1155
    public function getErrorCount()
×
1156
    {
1157
        return $this->errorCount;
×
1158

1159
    }//end getErrorCount()
1160

1161

1162
    /**
1163
     * Returns the number of warnings raised.
1164
     *
1165
     * @return int
1166
     */
1167
    public function getWarningCount()
×
1168
    {
1169
        return $this->warningCount;
×
1170

1171
    }//end getWarningCount()
1172

1173

1174
    /**
1175
     * Returns the number of fixable errors/warnings raised.
1176
     *
1177
     * @return int
1178
     */
1179
    public function getFixableCount()
×
1180
    {
1181
        return $this->fixableCount;
×
1182

1183
    }//end getFixableCount()
1184

1185

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

1195
    }//end getFixedCount()
1196

1197

1198
    /**
1199
     * Returns the list of ignored lines.
1200
     *
1201
     * @return array
1202
     */
1203
    public function getIgnoredLines()
×
1204
    {
1205
        return $this->tokenizer->ignoredLines;
×
1206

1207
    }//end getIgnoredLines()
1208

1209

1210
    /**
1211
     * Returns the errors raised from processing this file.
1212
     *
1213
     * @return array
1214
     */
1215
    public function getErrors()
×
1216
    {
1217
        return $this->errors;
×
1218

1219
    }//end getErrors()
1220

1221

1222
    /**
1223
     * Returns the warnings raised from processing this file.
1224
     *
1225
     * @return array
1226
     */
1227
    public function getWarnings()
×
1228
    {
1229
        return $this->warnings;
×
1230

1231
    }//end getWarnings()
1232

1233

1234
    /**
1235
     * Returns the metrics found while processing this file.
1236
     *
1237
     * @return array
1238
     */
1239
    public function getMetrics()
×
1240
    {
1241
        return $this->metrics;
×
1242

1243
    }//end getMetrics()
1244

1245

1246
    /**
1247
     * Returns the time taken processing this file for each invoked sniff.
1248
     *
1249
     * @return array
1250
     */
1251
    public function getListenerTimes()
×
1252
    {
1253
        return $this->listenerTimes;
×
1254

1255
    }//end getListenerTimes()
1256

1257

1258
    /**
1259
     * Returns the absolute filename of this file.
1260
     *
1261
     * @return string
1262
     */
1263
    public function getFilename()
×
1264
    {
1265
        return $this->path;
×
1266

1267
    }//end getFilename()
1268

1269

1270
    /**
1271
     * Returns the declaration name for classes, interfaces, traits, enums, and functions.
1272
     *
1273
     * @param int $stackPtr The position of the declaration token which
1274
     *                      declared the class, interface, trait, or function.
1275
     *
1276
     * @return string|null The name of the class, interface, trait, or function;
1277
     *                     or NULL if the function or class is anonymous.
1278
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type
1279
     *                                                      T_FUNCTION, T_CLASS, T_ANON_CLASS,
1280
     *                                                      T_CLOSURE, T_TRAIT, T_ENUM, or T_INTERFACE.
1281
     */
1282
    public function getDeclarationName($stackPtr)
105✔
1283
    {
1284
        $tokenCode = $this->tokens[$stackPtr]['code'];
105✔
1285

1286
        if ($tokenCode === T_ANON_CLASS || $tokenCode === T_CLOSURE) {
105✔
1287
            return null;
18✔
1288
        }
1289

1290
        if ($tokenCode !== T_FUNCTION
58✔
1291
            && $tokenCode !== T_CLASS
87✔
1292
            && $tokenCode !== T_INTERFACE
87✔
1293
            && $tokenCode !== T_TRAIT
87✔
1294
            && $tokenCode !== T_ENUM
87✔
1295
        ) {
29✔
1296
            throw new RuntimeException('Token type "'.$this->tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM');
6✔
1297
        }
1298

1299
        if ($tokenCode === T_FUNCTION
54✔
1300
            && strtolower($this->tokens[$stackPtr]['content']) !== 'function'
81✔
1301
        ) {
27✔
1302
            // This is a function declared without the "function" keyword.
1303
            // So this token is the function name.
1304
            return $this->tokens[$stackPtr]['content'];
3✔
1305
        }
1306

1307
        $stopPoint = $this->numTokens;
78✔
1308
        if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === true) {
78✔
1309
            // For functions, stop searching at the parenthesis opener.
1310
            $stopPoint = $this->tokens[$stackPtr]['parenthesis_opener'];
42✔
1311
        } else if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
50✔
1312
            // For OO tokens, stop searching at the open curly.
1313
            $stopPoint = $this->tokens[$stackPtr]['scope_opener'];
33✔
1314
        }
11✔
1315

1316
        $content = null;
78✔
1317
        for ($i = $stackPtr; $i < $stopPoint; $i++) {
78✔
1318
            if ($this->tokens[$i]['code'] === T_STRING) {
78✔
1319
                $content = $this->tokens[$i]['content'];
72✔
1320
                break;
72✔
1321
            }
1322
        }
26✔
1323

1324
        return $content;
78✔
1325

1326
    }//end getDeclarationName()
1327

1328

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

1391
        if ($this->tokens[$stackPtr]['code'] === T_USE) {
219✔
1392
            $opener = $this->findNext(T_OPEN_PARENTHESIS, ($stackPtr + 1));
18✔
1393
            if ($opener === false || isset($this->tokens[$opener]['parenthesis_owner']) === true) {
18✔
1394
                throw new RuntimeException('$stackPtr was not a valid T_USE');
12✔
1395
            }
1396
        } else {
3✔
1397
            if (isset($this->tokens[$stackPtr]['parenthesis_opener']) === false) {
201✔
1398
                // Live coding or syntax error, so no params to find.
1399
                return [];
3✔
1400
            }
1401

1402
            $opener = $this->tokens[$stackPtr]['parenthesis_opener'];
198✔
1403
        }
1404

1405
        if (isset($this->tokens[$opener]['parenthesis_closer']) === false) {
207✔
1406
            // Live coding or syntax error, so no params to find.
1407
            return [];
3✔
1408
        }
1409

1410
        $closer = $this->tokens[$opener]['parenthesis_closer'];
204✔
1411

1412
        $vars            = [];
204✔
1413
        $currVar         = null;
204✔
1414
        $paramStart      = ($opener + 1);
204✔
1415
        $defaultStart    = null;
204✔
1416
        $equalToken      = null;
204✔
1417
        $paramCount      = 0;
204✔
1418
        $hasAttributes   = false;
204✔
1419
        $passByReference = false;
204✔
1420
        $referenceToken  = false;
204✔
1421
        $variableLength  = false;
204✔
1422
        $variadicToken   = false;
204✔
1423
        $typeHint        = '';
204✔
1424
        $typeHintToken   = false;
204✔
1425
        $typeHintEndToken = false;
204✔
1426
        $nullableType     = false;
204✔
1427
        $visibilityToken  = null;
204✔
1428
        $readonlyToken    = null;
204✔
1429

1430
        for ($i = $paramStart; $i <= $closer; $i++) {
204✔
1431
            // Check to see if this token has a parenthesis or bracket opener. If it does
1432
            // it's likely to be an array which might have arguments in it. This
1433
            // could cause problems in our parsing below, so lets just skip to the
1434
            // end of it.
1435
            if ($this->tokens[$i]['code'] !== T_TYPE_OPEN_PARENTHESIS
204✔
1436
                && isset($this->tokens[$i]['parenthesis_opener']) === true
204✔
1437
            ) {
68✔
1438
                // Don't do this if it's the close parenthesis for the method.
1439
                if ($i !== $this->tokens[$i]['parenthesis_closer']) {
204✔
1440
                    $i = $this->tokens[$i]['parenthesis_closer'];
9✔
1441
                    continue;
9✔
1442
                }
1443
            }
68✔
1444

1445
            if (isset($this->tokens[$i]['bracket_opener']) === true) {
204✔
1446
                if ($i !== $this->tokens[$i]['bracket_closer']) {
3✔
1447
                    $i = $this->tokens[$i]['bracket_closer'];
3✔
1448
                    continue;
3✔
1449
                }
1450
            }
1451

1452
            switch ($this->tokens[$i]['code']) {
204✔
1453
            case T_ATTRIBUTE:
204✔
1454
                $hasAttributes = true;
6✔
1455

1456
                // Skip to the end of the attribute.
1457
                $i = $this->tokens[$i]['attribute_closer'];
6✔
1458
                break;
6✔
1459
            case T_BITWISE_AND:
204✔
1460
                if ($defaultStart === null) {
51✔
1461
                    $passByReference = true;
48✔
1462
                    $referenceToken  = $i;
48✔
1463
                }
16✔
1464
                break;
51✔
1465
            case T_VARIABLE:
204✔
1466
                $currVar = $i;
195✔
1467
                break;
195✔
1468
            case T_ELLIPSIS:
204✔
1469
                $variableLength = true;
51✔
1470
                $variadicToken  = $i;
51✔
1471
                break;
51✔
1472
            case T_CALLABLE:
204✔
1473
                if ($typeHintToken === false) {
12✔
1474
                    $typeHintToken = $i;
9✔
1475
                }
3✔
1476

1477
                $typeHint        .= $this->tokens[$i]['content'];
12✔
1478
                $typeHintEndToken = $i;
12✔
1479
                break;
12✔
1480
            case T_SELF:
204✔
1481
            case T_PARENT:
204✔
1482
            case T_STATIC:
204✔
1483
                // Self and parent are valid, static invalid, but was probably intended as type hint.
1484
                if (isset($defaultStart) === false) {
18✔
1485
                    if ($typeHintToken === false) {
15✔
1486
                        $typeHintToken = $i;
12✔
1487
                    }
4✔
1488

1489
                    $typeHint        .= $this->tokens[$i]['content'];
15✔
1490
                    $typeHintEndToken = $i;
15✔
1491
                }
5✔
1492
                break;
18✔
1493
            case T_STRING:
204✔
1494
                // This is a string, so it may be a type hint, but it could
1495
                // also be a constant used as a default value.
1496
                $prevComma = false;
138✔
1497
                for ($t = $i; $t >= $opener; $t--) {
138✔
1498
                    if ($this->tokens[$t]['code'] === T_COMMA) {
138✔
1499
                        $prevComma = $t;
60✔
1500
                        break;
60✔
1501
                    }
1502
                }
46✔
1503

1504
                if ($prevComma !== false) {
138✔
1505
                    $nextEquals = false;
60✔
1506
                    for ($t = $prevComma; $t < $i; $t++) {
60✔
1507
                        if ($this->tokens[$t]['code'] === T_EQUAL) {
60✔
1508
                            $nextEquals = $t;
9✔
1509
                            break;
9✔
1510
                        }
1511
                    }
20✔
1512

1513
                    if ($nextEquals !== false) {
60✔
1514
                        break;
9✔
1515
                    }
1516
                }
19✔
1517

1518
                if ($defaultStart === null) {
135✔
1519
                    if ($typeHintToken === false) {
132✔
1520
                        $typeHintToken = $i;
114✔
1521
                    }
38✔
1522

1523
                    $typeHint        .= $this->tokens[$i]['content'];
132✔
1524
                    $typeHintEndToken = $i;
132✔
1525
                }
44✔
1526
                break;
135✔
1527
            case T_NAMESPACE:
204✔
1528
            case T_NS_SEPARATOR:
204✔
1529
            case T_TYPE_UNION:
204✔
1530
            case T_TYPE_INTERSECTION:
204✔
1531
            case T_TYPE_OPEN_PARENTHESIS:
204✔
1532
            case T_TYPE_CLOSE_PARENTHESIS:
204✔
1533
            case T_FALSE:
204✔
1534
            case T_TRUE:
204✔
1535
            case T_NULL:
204✔
1536
                // Part of a type hint or default value.
1537
                if ($defaultStart === null) {
108✔
1538
                    if ($typeHintToken === false) {
99✔
1539
                        $typeHintToken = $i;
42✔
1540
                    }
14✔
1541

1542
                    $typeHint        .= $this->tokens[$i]['content'];
99✔
1543
                    $typeHintEndToken = $i;
99✔
1544
                }
33✔
1545
                break;
108✔
1546
            case T_NULLABLE:
204✔
1547
                if ($defaultStart === null) {
63✔
1548
                    $nullableType     = true;
63✔
1549
                    $typeHint        .= $this->tokens[$i]['content'];
63✔
1550
                    $typeHintEndToken = $i;
63✔
1551
                }
21✔
1552
                break;
63✔
1553
            case T_PUBLIC:
204✔
1554
            case T_PROTECTED:
204✔
1555
            case T_PRIVATE:
204✔
1556
                if ($defaultStart === null) {
24✔
1557
                    $visibilityToken = $i;
24✔
1558
                }
8✔
1559
                break;
24✔
1560
            case T_READONLY:
204✔
1561
                if ($defaultStart === null) {
9✔
1562
                    $readonlyToken = $i;
9✔
1563
                }
3✔
1564
                break;
9✔
1565
            case T_CLOSE_PARENTHESIS:
204✔
1566
            case T_COMMA:
194✔
1567
                // If it's null, then there must be no parameters for this
1568
                // method.
1569
                if ($currVar === null) {
204✔
1570
                    continue 2;
33✔
1571
                }
1572

1573
                $vars[$paramCount]            = [];
195✔
1574
                $vars[$paramCount]['token']   = $currVar;
195✔
1575
                $vars[$paramCount]['name']    = $this->tokens[$currVar]['content'];
195✔
1576
                $vars[$paramCount]['content'] = trim($this->getTokensAsString($paramStart, ($i - $paramStart)));
195✔
1577

1578
                if ($defaultStart !== null) {
195✔
1579
                    $vars[$paramCount]['default']       = trim($this->getTokensAsString($defaultStart, ($i - $defaultStart)));
66✔
1580
                    $vars[$paramCount]['default_token'] = $defaultStart;
66✔
1581
                    $vars[$paramCount]['default_equal_token'] = $equalToken;
66✔
1582
                }
22✔
1583

1584
                $vars[$paramCount]['has_attributes']      = $hasAttributes;
195✔
1585
                $vars[$paramCount]['pass_by_reference']   = $passByReference;
195✔
1586
                $vars[$paramCount]['reference_token']     = $referenceToken;
195✔
1587
                $vars[$paramCount]['variable_length']     = $variableLength;
195✔
1588
                $vars[$paramCount]['variadic_token']      = $variadicToken;
195✔
1589
                $vars[$paramCount]['type_hint']           = $typeHint;
195✔
1590
                $vars[$paramCount]['type_hint_token']     = $typeHintToken;
195✔
1591
                $vars[$paramCount]['type_hint_end_token'] = $typeHintEndToken;
195✔
1592
                $vars[$paramCount]['nullable_type']       = $nullableType;
195✔
1593

1594
                if ($visibilityToken !== null || $readonlyToken !== null) {
195✔
1595
                    $vars[$paramCount]['property_visibility'] = 'public';
27✔
1596
                    $vars[$paramCount]['visibility_token']    = false;
27✔
1597
                    $vars[$paramCount]['property_readonly']   = false;
27✔
1598

1599
                    if ($visibilityToken !== null) {
27✔
1600
                        $vars[$paramCount]['property_visibility'] = $this->tokens[$visibilityToken]['content'];
24✔
1601
                        $vars[$paramCount]['visibility_token']    = $visibilityToken;
24✔
1602
                    }
8✔
1603

1604
                    if ($readonlyToken !== null) {
27✔
1605
                        $vars[$paramCount]['property_readonly'] = true;
9✔
1606
                        $vars[$paramCount]['readonly_token']    = $readonlyToken;
9✔
1607
                    }
3✔
1608
                }
9✔
1609

1610
                if ($this->tokens[$i]['code'] === T_COMMA) {
195✔
1611
                    $vars[$paramCount]['comma_token'] = $i;
96✔
1612
                } else {
32✔
1613
                    $vars[$paramCount]['comma_token'] = false;
171✔
1614
                }
1615

1616
                // Reset the vars, as we are about to process the next parameter.
1617
                $currVar          = null;
195✔
1618
                $paramStart       = ($i + 1);
195✔
1619
                $defaultStart     = null;
195✔
1620
                $equalToken       = null;
195✔
1621
                $hasAttributes    = false;
195✔
1622
                $passByReference  = false;
195✔
1623
                $referenceToken   = false;
195✔
1624
                $variableLength   = false;
195✔
1625
                $variadicToken    = false;
195✔
1626
                $typeHint         = '';
195✔
1627
                $typeHintToken    = false;
195✔
1628
                $typeHintEndToken = false;
195✔
1629
                $nullableType     = false;
195✔
1630
                $visibilityToken  = null;
195✔
1631
                $readonlyToken    = null;
195✔
1632

1633
                $paramCount++;
195✔
1634
                break;
195✔
1635
            case T_EQUAL:
189✔
1636
                $defaultStart = $this->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
66✔
1637
                $equalToken   = $i;
66✔
1638
                break;
66✔
1639
            }//end switch
65✔
1640
        }//end for
65✔
1641

1642
        return $vars;
204✔
1643

1644
    }//end getMethodParameters()
1645

1646

1647
    /**
1648
     * Returns the visibility and implementation properties of a method.
1649
     *
1650
     * The format of the return value is:
1651
     * <code>
1652
     *   array(
1653
     *    'scope'                 => string,        // Public, private, or protected
1654
     *    'scope_specified'       => boolean,       // TRUE if the scope keyword was found.
1655
     *    'return_type'           => string,        // The return type of the method.
1656
     *    'return_type_token'     => integer|false, // The stack pointer to the start of the return type
1657
     *                                              // or FALSE if there is no return type.
1658
     *    'return_type_end_token' => integer|false, // The stack pointer to the end of the return type
1659
     *                                              // or FALSE if there is no return type.
1660
     *    'nullable_return_type'  => boolean,       // TRUE if the return type is preceded by the
1661
     *                                              // nullability operator.
1662
     *    'is_abstract'           => boolean,       // TRUE if the abstract keyword was found.
1663
     *    'is_final'              => boolean,       // TRUE if the final keyword was found.
1664
     *    'is_static'             => boolean,       // TRUE if the static keyword was found.
1665
     *    'has_body'              => boolean,       // TRUE if the method has a body
1666
     *   );
1667
     * </code>
1668
     *
1669
     * @param int $stackPtr The position in the stack of the function token to
1670
     *                      acquire the properties for.
1671
     *
1672
     * @return array
1673
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1674
     *                                                      T_FUNCTION, T_CLOSURE, or T_FN token.
1675
     */
1676
    public function getMethodProperties($stackPtr)
177✔
1677
    {
1678
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
177✔
1679
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
177✔
1680
            && $this->tokens[$stackPtr]['code'] !== T_FN
177✔
1681
        ) {
59✔
1682
            throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_FN');
9✔
1683
        }
1684

1685
        if ($this->tokens[$stackPtr]['code'] === T_FUNCTION) {
168✔
1686
            $valid = [
41✔
1687
                T_PUBLIC      => T_PUBLIC,
123✔
1688
                T_PRIVATE     => T_PRIVATE,
123✔
1689
                T_PROTECTED   => T_PROTECTED,
123✔
1690
                T_STATIC      => T_STATIC,
123✔
1691
                T_FINAL       => T_FINAL,
123✔
1692
                T_ABSTRACT    => T_ABSTRACT,
123✔
1693
                T_WHITESPACE  => T_WHITESPACE,
123✔
1694
                T_COMMENT     => T_COMMENT,
123✔
1695
                T_DOC_COMMENT => T_DOC_COMMENT,
123✔
1696
            ];
82✔
1697
        } else {
41✔
1698
            $valid = [
15✔
1699
                T_STATIC      => T_STATIC,
45✔
1700
                T_WHITESPACE  => T_WHITESPACE,
45✔
1701
                T_COMMENT     => T_COMMENT,
45✔
1702
                T_DOC_COMMENT => T_DOC_COMMENT,
45✔
1703
            ];
30✔
1704
        }
1705

1706
        $scope          = 'public';
168✔
1707
        $scopeSpecified = false;
168✔
1708
        $isAbstract     = false;
168✔
1709
        $isFinal        = false;
168✔
1710
        $isStatic       = false;
168✔
1711

1712
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
168✔
1713
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
168✔
1714
                break;
165✔
1715
            }
1716

1717
            switch ($this->tokens[$i]['code']) {
165✔
1718
            case T_PUBLIC:
165✔
1719
                $scope          = 'public';
18✔
1720
                $scopeSpecified = true;
18✔
1721
                break;
18✔
1722
            case T_PRIVATE:
165✔
1723
                $scope          = 'private';
9✔
1724
                $scopeSpecified = true;
9✔
1725
                break;
9✔
1726
            case T_PROTECTED:
165✔
1727
                $scope          = 'protected';
9✔
1728
                $scopeSpecified = true;
9✔
1729
                break;
9✔
1730
            case T_ABSTRACT:
165✔
1731
                $isAbstract = true;
9✔
1732
                break;
9✔
1733
            case T_FINAL:
165✔
1734
                $isFinal = true;
3✔
1735
                break;
3✔
1736
            case T_STATIC:
165✔
1737
                $isStatic = true;
6✔
1738
                break;
6✔
1739
            }//end switch
55✔
1740
        }//end for
55✔
1741

1742
        $returnType         = '';
168✔
1743
        $returnTypeToken    = false;
168✔
1744
        $returnTypeEndToken = false;
168✔
1745
        $nullableReturnType = false;
168✔
1746
        $hasBody            = true;
168✔
1747

1748
        if (isset($this->tokens[$stackPtr]['parenthesis_closer']) === true) {
168✔
1749
            $scopeOpener = null;
168✔
1750
            if (isset($this->tokens[$stackPtr]['scope_opener']) === true) {
168✔
1751
                $scopeOpener = $this->tokens[$stackPtr]['scope_opener'];
153✔
1752
            }
51✔
1753

1754
            $valid = [
56✔
1755
                T_STRING                 => T_STRING,
168✔
1756
                T_CALLABLE               => T_CALLABLE,
168✔
1757
                T_SELF                   => T_SELF,
168✔
1758
                T_PARENT                 => T_PARENT,
168✔
1759
                T_STATIC                 => T_STATIC,
168✔
1760
                T_FALSE                  => T_FALSE,
168✔
1761
                T_TRUE                   => T_TRUE,
168✔
1762
                T_NULL                   => T_NULL,
168✔
1763
                T_NAMESPACE              => T_NAMESPACE,
168✔
1764
                T_NS_SEPARATOR           => T_NS_SEPARATOR,
168✔
1765
                T_TYPE_UNION             => T_TYPE_UNION,
168✔
1766
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
168✔
1767
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
168✔
1768
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
168✔
1769
            ];
112✔
1770

1771
            for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) {
168✔
1772
                if (($scopeOpener === null && $this->tokens[$i]['code'] === T_SEMICOLON)
168✔
1773
                    || ($scopeOpener !== null && $i === $scopeOpener)
168✔
1774
                ) {
56✔
1775
                    // End of function definition.
1776
                    break;
168✔
1777
                }
1778

1779
                if ($this->tokens[$i]['code'] === T_USE) {
168✔
1780
                    // Skip over closure use statements.
1781
                    for ($j = ($i + 1); $j < $this->numTokens && isset(Tokens::$emptyTokens[$this->tokens[$j]['code']]) === true; $j++);
15✔
1782
                    if ($this->tokens[$j]['code'] === T_OPEN_PARENTHESIS) {
15✔
1783
                        if (isset($this->tokens[$j]['parenthesis_closer']) === false) {
15✔
1784
                            // Live coding/parse error, stop parsing.
1785
                            break;
×
1786
                        }
1787

1788
                        $i = $this->tokens[$j]['parenthesis_closer'];
15✔
1789
                        continue;
15✔
1790
                    }
1791
                }
1792

1793
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
168✔
1794
                    $nullableReturnType = true;
39✔
1795
                }
13✔
1796

1797
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
168✔
1798
                    if ($returnTypeToken === false) {
144✔
1799
                        $returnTypeToken = $i;
144✔
1800
                    }
48✔
1801

1802
                    $returnType        .= $this->tokens[$i]['content'];
144✔
1803
                    $returnTypeEndToken = $i;
144✔
1804
                }
48✔
1805
            }//end for
56✔
1806

1807
            if ($this->tokens[$stackPtr]['code'] === T_FN) {
168✔
1808
                $bodyToken = T_FN_ARROW;
18✔
1809
            } else {
6✔
1810
                $bodyToken = T_OPEN_CURLY_BRACKET;
150✔
1811
            }
1812

1813
            $end     = $this->findNext([$bodyToken, T_SEMICOLON], $this->tokens[$stackPtr]['parenthesis_closer']);
168✔
1814
            $hasBody = $this->tokens[$end]['code'] === $bodyToken;
168✔
1815
        }//end if
56✔
1816

1817
        if ($returnType !== '' && $nullableReturnType === true) {
168✔
1818
            $returnType = '?'.$returnType;
39✔
1819
        }
13✔
1820

1821
        return [
56✔
1822
            'scope'                 => $scope,
168✔
1823
            'scope_specified'       => $scopeSpecified,
168✔
1824
            'return_type'           => $returnType,
168✔
1825
            'return_type_token'     => $returnTypeToken,
168✔
1826
            'return_type_end_token' => $returnTypeEndToken,
168✔
1827
            'nullable_return_type'  => $nullableReturnType,
168✔
1828
            'is_abstract'           => $isAbstract,
168✔
1829
            'is_final'              => $isFinal,
168✔
1830
            'is_static'             => $isStatic,
168✔
1831
            'has_body'              => $hasBody,
168✔
1832
        ];
112✔
1833

1834
    }//end getMethodProperties()
1835

1836

1837
    /**
1838
     * Returns the visibility and implementation properties of a class member var.
1839
     *
1840
     * The format of the return value is:
1841
     *
1842
     * <code>
1843
     *   array(
1844
     *    'scope'           => string,        // Public, private, or protected.
1845
     *    'scope_specified' => boolean,       // TRUE if the scope was explicitly specified.
1846
     *    'is_static'       => boolean,       // TRUE if the static keyword was found.
1847
     *    'is_readonly'     => boolean,       // TRUE if the readonly keyword was found.
1848
     *    'is_final'        => boolean,       // TRUE if the final keyword was found.
1849
     *    'type'            => string,        // The type of the var (empty if no type specified).
1850
     *    'type_token'      => integer|false, // The stack pointer to the start of the type
1851
     *                                        // or FALSE if there is no type.
1852
     *    'type_end_token'  => integer|false, // The stack pointer to the end of the type
1853
     *                                        // or FALSE if there is no type.
1854
     *    'nullable_type'   => boolean,       // TRUE if the type is preceded by the nullability
1855
     *                                        // operator.
1856
     *   );
1857
     * </code>
1858
     *
1859
     * @param int $stackPtr The position in the stack of the T_VARIABLE token to
1860
     *                      acquire the properties for.
1861
     *
1862
     * @return array
1863
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
1864
     *                                                      T_VARIABLE token, or if the position is not
1865
     *                                                      a class member variable.
1866
     */
1867
    public function getMemberProperties($stackPtr)
300✔
1868
    {
1869
        if ($this->tokens[$stackPtr]['code'] !== T_VARIABLE) {
300✔
1870
            throw new RuntimeException('$stackPtr must be of type T_VARIABLE');
3✔
1871
        }
1872

1873
        $conditions = array_keys($this->tokens[$stackPtr]['conditions']);
297✔
1874
        $ptr        = array_pop($conditions);
297✔
1875
        if (isset($this->tokens[$ptr]) === false
297✔
1876
            || ($this->tokens[$ptr]['code'] !== T_CLASS
296✔
1877
            && $this->tokens[$ptr]['code'] !== T_ANON_CLASS
295✔
1878
            && $this->tokens[$ptr]['code'] !== T_TRAIT)
296✔
1879
        ) {
99✔
1880
            if (isset($this->tokens[$ptr]) === true
18✔
1881
                && ($this->tokens[$ptr]['code'] === T_INTERFACE
17✔
1882
                || $this->tokens[$ptr]['code'] === T_ENUM)
17✔
1883
            ) {
6✔
1884
                // T_VARIABLEs in interfaces/enums can actually be method arguments
1885
                // but they won't be seen as being inside the method because there
1886
                // are no scope openers and closers for abstract methods. If it is in
1887
                // parentheses, we can be pretty sure it is a method argument.
1888
                if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === false
9✔
1889
                    || empty($this->tokens[$stackPtr]['nested_parenthesis']) === true
9✔
1890
                ) {
3✔
1891
                    $error = 'Possible parse error: %ss may not include member vars';
6✔
1892
                    $code  = sprintf('Internal.ParseError.%sHasMemberVar', ucfirst($this->tokens[$ptr]['content']));
6✔
1893
                    $data  = [strtolower($this->tokens[$ptr]['content'])];
6✔
1894
                    $this->addWarning($error, $stackPtr, $code, $data);
6✔
1895
                    return [];
7✔
1896
                }
1897
            } else {
1✔
1898
                throw new RuntimeException('$stackPtr is not a class member var');
9✔
1899
            }
1900
        }//end if
1✔
1901

1902
        // Make sure it's not a method parameter.
1903
        if (empty($this->tokens[$stackPtr]['nested_parenthesis']) === false) {
282✔
1904
            $parenthesis = array_keys($this->tokens[$stackPtr]['nested_parenthesis']);
18✔
1905
            $deepestOpen = array_pop($parenthesis);
18✔
1906
            if ($deepestOpen > $ptr
12✔
1907
                && isset($this->tokens[$deepestOpen]['parenthesis_owner']) === true
18✔
1908
                && $this->tokens[$this->tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION
18✔
1909
            ) {
6✔
1910
                throw new RuntimeException('$stackPtr is not a class member var');
12✔
1911
            }
1912
        }
2✔
1913

1914
        $valid = [
90✔
1915
            T_PUBLIC    => T_PUBLIC,
270✔
1916
            T_PRIVATE   => T_PRIVATE,
270✔
1917
            T_PROTECTED => T_PROTECTED,
270✔
1918
            T_STATIC    => T_STATIC,
270✔
1919
            T_VAR       => T_VAR,
270✔
1920
            T_READONLY  => T_READONLY,
270✔
1921
            T_FINAL     => T_FINAL,
270✔
1922
        ];
180✔
1923

1924
        $valid += Tokens::$emptyTokens;
270✔
1925

1926
        $scope          = 'public';
270✔
1927
        $scopeSpecified = false;
270✔
1928
        $isStatic       = false;
270✔
1929
        $isReadonly     = false;
270✔
1930
        $isFinal        = false;
270✔
1931

1932
        $startOfStatement = $this->findPrevious(
270✔
1933
            [
90✔
1934
                T_SEMICOLON,
270✔
1935
                T_OPEN_CURLY_BRACKET,
270✔
1936
                T_CLOSE_CURLY_BRACKET,
270✔
1937
                T_ATTRIBUTE_END,
270✔
1938
            ],
180✔
1939
            ($stackPtr - 1)
270✔
1940
        );
180✔
1941

1942
        for ($i = ($startOfStatement + 1); $i < $stackPtr; $i++) {
270✔
1943
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
270✔
1944
                break;
207✔
1945
            }
1946

1947
            switch ($this->tokens[$i]['code']) {
270✔
1948
            case T_PUBLIC:
270✔
1949
                $scope          = 'public';
129✔
1950
                $scopeSpecified = true;
129✔
1951
                break;
129✔
1952
            case T_PRIVATE:
270✔
1953
                $scope          = 'private';
57✔
1954
                $scopeSpecified = true;
57✔
1955
                break;
57✔
1956
            case T_PROTECTED:
270✔
1957
                $scope          = 'protected';
42✔
1958
                $scopeSpecified = true;
42✔
1959
                break;
42✔
1960
            case T_STATIC:
270✔
1961
                $isStatic = true;
66✔
1962
                break;
66✔
1963
            case T_READONLY:
270✔
1964
                $isReadonly = true;
36✔
1965
                break;
36✔
1966
            case T_FINAL:
270✔
1967
                $isFinal = true;
27✔
1968
                break;
27✔
1969
            }//end switch
90✔
1970
        }//end for
90✔
1971

1972
        $type         = '';
270✔
1973
        $typeToken    = false;
270✔
1974
        $typeEndToken = false;
270✔
1975
        $nullableType = false;
270✔
1976

1977
        if ($i < $stackPtr) {
270✔
1978
            // We've found a type.
1979
            $valid = [
69✔
1980
                T_STRING                 => T_STRING,
207✔
1981
                T_CALLABLE               => T_CALLABLE,
207✔
1982
                T_SELF                   => T_SELF,
207✔
1983
                T_PARENT                 => T_PARENT,
207✔
1984
                T_FALSE                  => T_FALSE,
207✔
1985
                T_TRUE                   => T_TRUE,
207✔
1986
                T_NULL                   => T_NULL,
207✔
1987
                T_NAMESPACE              => T_NAMESPACE,
207✔
1988
                T_NS_SEPARATOR           => T_NS_SEPARATOR,
207✔
1989
                T_TYPE_UNION             => T_TYPE_UNION,
207✔
1990
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
207✔
1991
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
207✔
1992
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
207✔
1993
            ];
138✔
1994

1995
            for ($i; $i < $stackPtr; $i++) {
207✔
1996
                if ($this->tokens[$i]['code'] === T_VARIABLE) {
207✔
1997
                    // Hit another variable in a group definition.
1998
                    break;
30✔
1999
                }
2000

2001
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
183✔
2002
                    $nullableType = true;
51✔
2003
                }
17✔
2004

2005
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
183✔
2006
                    $typeEndToken = $i;
183✔
2007
                    if ($typeToken === false) {
183✔
2008
                        $typeToken = $i;
183✔
2009
                    }
61✔
2010

2011
                    $type .= $this->tokens[$i]['content'];
183✔
2012
                }
61✔
2013
            }
61✔
2014

2015
            if ($type !== '' && $nullableType === true) {
207✔
2016
                $type = '?'.$type;
51✔
2017
            }
17✔
2018
        }//end if
69✔
2019

2020
        return [
90✔
2021
            'scope'           => $scope,
270✔
2022
            'scope_specified' => $scopeSpecified,
270✔
2023
            'is_static'       => $isStatic,
270✔
2024
            'is_readonly'     => $isReadonly,
270✔
2025
            'is_final'        => $isFinal,
270✔
2026
            'type'            => $type,
270✔
2027
            'type_token'      => $typeToken,
270✔
2028
            'type_end_token'  => $typeEndToken,
270✔
2029
            'nullable_type'   => $nullableType,
270✔
2030
        ];
180✔
2031

2032
    }//end getMemberProperties()
2033

2034

2035
    /**
2036
     * Returns the visibility and implementation properties of a class.
2037
     *
2038
     * The format of the return value is:
2039
     * <code>
2040
     *   array(
2041
     *    'is_abstract' => boolean, // TRUE if the abstract keyword was found.
2042
     *    'is_final'    => boolean, // TRUE if the final keyword was found.
2043
     *    'is_readonly' => boolean, // TRUE if the readonly keyword was found.
2044
     *   );
2045
     * </code>
2046
     *
2047
     * @param int $stackPtr The position in the stack of the T_CLASS token to
2048
     *                      acquire the properties for.
2049
     *
2050
     * @return array
2051
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a
2052
     *                                                      T_CLASS token.
2053
     */
2054
    public function getClassProperties($stackPtr)
42✔
2055
    {
2056
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS) {
42✔
2057
            throw new RuntimeException('$stackPtr must be of type T_CLASS');
9✔
2058
        }
2059

2060
        $valid = [
11✔
2061
            T_FINAL       => T_FINAL,
33✔
2062
            T_ABSTRACT    => T_ABSTRACT,
33✔
2063
            T_READONLY    => T_READONLY,
33✔
2064
            T_WHITESPACE  => T_WHITESPACE,
33✔
2065
            T_COMMENT     => T_COMMENT,
33✔
2066
            T_DOC_COMMENT => T_DOC_COMMENT,
33✔
2067
        ];
22✔
2068

2069
        $isAbstract = false;
33✔
2070
        $isFinal    = false;
33✔
2071
        $isReadonly = false;
33✔
2072

2073
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
33✔
2074
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
33✔
2075
                break;
33✔
2076
            }
2077

2078
            switch ($this->tokens[$i]['code']) {
33✔
2079
            case T_ABSTRACT:
33✔
2080
                $isAbstract = true;
15✔
2081
                break;
15✔
2082

2083
            case T_FINAL:
33✔
2084
                $isFinal = true;
12✔
2085
                break;
12✔
2086

2087
            case T_READONLY:
33✔
2088
                $isReadonly = true;
15✔
2089
                break;
15✔
2090
            }
11✔
2091
        }//end for
11✔
2092

2093
        return [
11✔
2094
            'is_abstract' => $isAbstract,
33✔
2095
            'is_final'    => $isFinal,
33✔
2096
            'is_readonly' => $isReadonly,
33✔
2097
        ];
22✔
2098

2099
    }//end getClassProperties()
2100

2101

2102
    /**
2103
     * Determine if the passed token is a reference operator.
2104
     *
2105
     * Returns true if the specified token position represents a reference.
2106
     * Returns false if the token represents a bitwise operator.
2107
     *
2108
     * @param int $stackPtr The position of the T_BITWISE_AND token.
2109
     *
2110
     * @return boolean
2111
     */
2112
    public function isReference($stackPtr)
228✔
2113
    {
2114
        if ($this->tokens[$stackPtr]['code'] !== T_BITWISE_AND) {
228✔
2115
            return false;
9✔
2116
        }
2117

2118
        $tokenBefore = $this->findPrevious(
219✔
2119
            Tokens::$emptyTokens,
219✔
2120
            ($stackPtr - 1),
219✔
2121
            null,
219✔
2122
            true
146✔
2123
        );
146✔
2124

2125
        if ($this->tokens[$tokenBefore]['code'] === T_FUNCTION
219✔
2126
            || $this->tokens[$tokenBefore]['code'] === T_CLOSURE
217✔
2127
            || $this->tokens[$tokenBefore]['code'] === T_FN
218✔
2128
        ) {
73✔
2129
            // Function returns a reference.
2130
            return true;
9✔
2131
        }
2132

2133
        if ($this->tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
210✔
2134
            // Inside a foreach loop or array assignment, this is a reference.
2135
            return true;
18✔
2136
        }
2137

2138
        if ($this->tokens[$tokenBefore]['code'] === T_AS) {
192✔
2139
            // Inside a foreach loop, this is a reference.
2140
            return true;
3✔
2141
        }
2142

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

2149
        $tokenAfter = $this->findNext(
168✔
2150
            Tokens::$emptyTokens,
168✔
2151
            ($stackPtr + 1),
168✔
2152
            null,
168✔
2153
            true
112✔
2154
        );
112✔
2155

2156
        if ($this->tokens[$tokenAfter]['code'] === T_NEW) {
168✔
2157
            return true;
3✔
2158
        }
2159

2160
        if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === true) {
165✔
2161
            $brackets    = $this->tokens[$stackPtr]['nested_parenthesis'];
117✔
2162
            $lastBracket = array_pop($brackets);
117✔
2163
            if (isset($this->tokens[$lastBracket]['parenthesis_owner']) === true) {
117✔
2164
                $owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']];
72✔
2165
                if ($owner['code'] === T_FUNCTION
72✔
2166
                    || $owner['code'] === T_CLOSURE
64✔
2167
                    || $owner['code'] === T_FN
68✔
2168
                ) {
24✔
2169
                    $params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']);
51✔
2170
                    foreach ($params as $param) {
58✔
2171
                        if ($param['reference_token'] === $stackPtr) {
51✔
2172
                            // Function parameter declared to be passed by reference.
2173
                            return true;
36✔
2174
                        }
2175
                    }
11✔
2176
                }//end if
5✔
2177
            } else {
12✔
2178
                $prev = false;
45✔
2179
                for ($t = ($this->tokens[$lastBracket]['parenthesis_opener'] - 1); $t >= 0; $t--) {
45✔
2180
                    if ($this->tokens[$t]['code'] !== T_WHITESPACE) {
45✔
2181
                        $prev = $t;
45✔
2182
                        break;
45✔
2183
                    }
2184
                }
3✔
2185

2186
                if ($prev !== false && $this->tokens[$prev]['code'] === T_USE) {
45✔
2187
                    // Closure use by reference.
2188
                    return true;
3✔
2189
                }
2190
            }//end if
2191
        }//end if
26✔
2192

2193
        // Pass by reference in function calls and assign by reference in arrays.
2194
        if ($this->tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
126✔
2195
            || $this->tokens[$tokenBefore]['code'] === T_COMMA
116✔
2196
            || $this->tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY
121✔
2197
        ) {
42✔
2198
            if ($this->tokens[$tokenAfter]['code'] === T_VARIABLE) {
90✔
2199
                return true;
66✔
2200
            } else {
2201
                $skip   = Tokens::$emptyTokens;
24✔
2202
                $skip[] = T_NS_SEPARATOR;
24✔
2203
                $skip[] = T_SELF;
24✔
2204
                $skip[] = T_PARENT;
24✔
2205
                $skip[] = T_STATIC;
24✔
2206
                $skip[] = T_STRING;
24✔
2207
                $skip[] = T_NAMESPACE;
24✔
2208
                $skip[] = T_DOUBLE_COLON;
24✔
2209

2210
                $nextSignificantAfter = $this->findNext(
24✔
2211
                    $skip,
24✔
2212
                    ($stackPtr + 1),
24✔
2213
                    null,
24✔
2214
                    true
16✔
2215
                );
16✔
2216
                if ($this->tokens[$nextSignificantAfter]['code'] === T_VARIABLE) {
24✔
2217
                    return true;
24✔
2218
                }
2219
            }//end if
2220
        }//end if
2221

2222
        return false;
36✔
2223

2224
    }//end isReference()
2225

2226

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

2245
        if (is_int($length) === false || $length <= 0) {
78✔
2246
            return '';
9✔
2247
        }
2248

2249
        $str = '';
69✔
2250
        $end = ($start + $length);
69✔
2251
        if ($end > $this->numTokens) {
69✔
2252
            $end = $this->numTokens;
3✔
2253
        }
1✔
2254

2255
        for ($i = $start; $i < $end; $i++) {
69✔
2256
            // If tabs are being converted to spaces by the tokeniser, the
2257
            // original content should be used instead of the converted content.
2258
            if ($origContent === true && isset($this->tokens[$i]['orig_content']) === true) {
69✔
2259
                $str .= $this->tokens[$i]['orig_content'];
6✔
2260
            } else {
2✔
2261
                $str .= $this->tokens[$i]['content'];
69✔
2262
            }
2263
        }
23✔
2264

2265
        return $str;
69✔
2266

2267
    }//end getTokensAsString()
2268

2269

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

2306
        if ($end === null) {
×
2307
            $end = 0;
×
2308
        }
2309

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

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

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

2346
        return false;
×
2347

2348
    }//end findPrevious()
2349

2350

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

2387
        if ($end === null || $end > $this->numTokens) {
×
2388
            $end = $this->numTokens;
×
2389
        }
2390

2391
        for ($i = $start; $i < $end; $i++) {
×
2392
            $found = (bool) $exclude;
×
2393
            foreach ($types as $type) {
×
2394
                if ($this->tokens[$i]['code'] === $type) {
×
2395
                    $found = !$exclude;
×
2396
                    break;
×
2397
                }
2398
            }
2399

2400
            if ($found === true) {
×
2401
                if ($value === null) {
×
2402
                    return $i;
×
2403
                } else if ($this->tokens[$i]['content'] === $value) {
×
2404
                    return $i;
×
2405
                }
2406
            }
2407

2408
            if ($local === true && $this->tokens[$i]['code'] === T_SEMICOLON) {
×
2409
                break;
×
2410
            }
2411
        }//end for
2412

2413
        return false;
×
2414

2415
    }//end findNext()
2416

2417

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

2433
        $endTokens = [
68✔
2434
            T_CLOSE_TAG    => true,
204✔
2435
            T_COLON        => true,
204✔
2436
            T_COMMA        => true,
204✔
2437
            T_DOUBLE_ARROW => true,
204✔
2438
            T_MATCH_ARROW  => true,
204✔
2439
            T_SEMICOLON    => true,
204✔
2440
        ];
136✔
2441

2442
        if ($ignore !== null) {
204✔
2443
            $ignore = (array) $ignore;
×
2444
            foreach ($ignore as $code) {
×
2445
                if (isset($startTokens[$code]) === true) {
×
2446
                    unset($startTokens[$code]);
×
2447
                }
2448

2449
                if (isset($endTokens[$code]) === true) {
×
2450
                    unset($endTokens[$code]);
×
2451
                }
2452
            }
2453
        }
2454

2455
        // If the start token is inside the case part of a match expression,
2456
        // find the start of the condition. If it's in the statement part, find
2457
        // the token that comes after the match arrow.
2458
        if (empty($this->tokens[$start]['conditions']) === false) {
204✔
2459
            $conditions         = $this->tokens[$start]['conditions'];
156✔
2460
            $lastConditionOwner = end($conditions);
156✔
2461
            $matchExpression    = key($conditions);
156✔
2462

2463
            if ($lastConditionOwner === T_MATCH
104✔
2464
                // Check if the $start token is at the same parentheses nesting level as the match token.
2465
                && ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === true
135✔
2466
                && empty($this->tokens[$start]['nested_parenthesis']) === true)
106✔
2467
                || ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === false
86✔
2468
                && empty($this->tokens[$start]['nested_parenthesis']) === false)
86✔
2469
                && $this->tokens[$matchExpression]['nested_parenthesis'] === $this->tokens[$start]['nested_parenthesis']))
121✔
2470
            ) {
52✔
2471
                // Walk back to the previous match arrow (if it exists).
2472
                $lastComma          = null;
45✔
2473
                $inNestedExpression = false;
45✔
2474
                for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
45✔
2475
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_MATCH_ARROW) {
45✔
2476
                        break;
33✔
2477
                    }
2478

2479
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_COMMA) {
45✔
2480
                        $lastComma = $prevMatch;
24✔
2481
                        continue;
24✔
2482
                    }
2483

2484
                    // Skip nested statements.
2485
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
45✔
2486
                        && $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
45✔
2487
                    ) {
15✔
2488
                        $prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
12✔
2489
                        continue;
12✔
2490
                    }
2491

2492
                    if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
45✔
2493
                        && $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
45✔
2494
                    ) {
15✔
2495
                        $prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
15✔
2496
                        continue;
15✔
2497
                    }
2498

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

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

2520
                        return $next;
12✔
2521
                    }
2522

2523
                    // Okay, so we found a match arrow.
2524
                    // If $start was part of the "next" condition, the last comma will be set.
2525
                    // Otherwise, $start must have been part of a return expression.
2526
                    if (isset($lastComma) === true && $lastComma > $prevMatch) {
33✔
2527
                        $prevMatch = $lastComma;
15✔
2528
                    }
5✔
2529

2530
                    // In both cases, go to the first non-empty token after.
2531
                    $next = $this->findNext(Tokens::$emptyTokens, ($prevMatch + 1), null, true);
33✔
2532
                    if ($next === false) {
33✔
2533
                        // Shouldn't be possible.
2534
                        return $start;
×
2535
                    }
2536

2537
                    return $next;
33✔
2538
                }//end if
2539
            }//end if
5✔
2540
        }//end if
42✔
2541

2542
        $lastNotEmpty = $start;
174✔
2543

2544
        // If we are starting at a token that ends a scope block, skip to
2545
        // the start and continue from there.
2546
        // If we are starting at a token that ends a statement, skip this
2547
        // token so we find the true start of the statement.
2548
        while (isset($endTokens[$this->tokens[$start]['code']]) === true
174✔
2549
            || (isset($this->tokens[$start]['scope_condition']) === true
174✔
2550
            && $start === $this->tokens[$start]['scope_closer'])
174✔
2551
        ) {
58✔
2552
            if (isset($this->tokens[$start]['scope_condition']) === true) {
48✔
2553
                $start = $this->tokens[$start]['scope_condition'];
27✔
2554
            } else {
9✔
2555
                $start--;
27✔
2556
            }
2557
        }
16✔
2558

2559
        for ($i = $start; $i >= 0; $i--) {
174✔
2560
            if (isset($startTokens[$this->tokens[$i]['code']]) === true
174✔
2561
                || isset($endTokens[$this->tokens[$i]['code']]) === true
174✔
2562
            ) {
58✔
2563
                // Found the end of the previous statement.
2564
                return $lastNotEmpty;
174✔
2565
            }
2566

2567
            if (isset($this->tokens[$i]['scope_opener']) === true
171✔
2568
                && $i === $this->tokens[$i]['scope_closer']
171✔
2569
                && $this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS
171✔
2570
                && $this->tokens[$i]['code'] !== T_END_NOWDOC
171✔
2571
                && $this->tokens[$i]['code'] !== T_END_HEREDOC
171✔
2572
                && $this->tokens[$i]['code'] !== T_BREAK
171✔
2573
                && $this->tokens[$i]['code'] !== T_RETURN
171✔
2574
                && $this->tokens[$i]['code'] !== T_CONTINUE
171✔
2575
                && $this->tokens[$i]['code'] !== T_THROW
171✔
2576
                && $this->tokens[$i]['code'] !== T_EXIT
171✔
2577
            ) {
57✔
2578
                // Found the end of the previous scope block.
2579
                return $lastNotEmpty;
3✔
2580
            }
2581

2582
            // Skip nested statements.
2583
            if (isset($this->tokens[$i]['bracket_opener']) === true
171✔
2584
                && $i === $this->tokens[$i]['bracket_closer']
171✔
2585
            ) {
57✔
2586
                $i = $this->tokens[$i]['bracket_opener'];
3✔
2587
            } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
171✔
2588
                && $i === $this->tokens[$i]['parenthesis_closer']
171✔
2589
            ) {
57✔
2590
                $i = $this->tokens[$i]['parenthesis_opener'];
21✔
2591
            } else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
171✔
2592
                $start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
6✔
2593
                if ($start !== false) {
6✔
2594
                    $i = $start;
6✔
2595
                }
2✔
2596
            }//end if
2✔
2597

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

2603
        return 0;
×
2604

2605
    }//end findStartOfStatement()
2606

2607

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

2631
        if ($ignore !== null) {
66✔
2632
            $ignore = (array) $ignore;
×
2633
            foreach ($ignore as $code) {
×
2634
                unset($endTokens[$code]);
×
2635
            }
2636
        }
2637

2638
        // If the start token is inside the case part of a match expression,
2639
        // advance to the match arrow and continue looking for the
2640
        // end of the statement from there so that we skip over commas.
2641
        if ($this->tokens[$start]['code'] !== T_MATCH_ARROW) {
66✔
2642
            $matchExpression = $this->getCondition($start, T_MATCH);
66✔
2643
            if ($matchExpression !== false) {
66✔
2644
                $beforeArrow    = true;
30✔
2645
                $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']);
30✔
2646
                if ($prevMatchArrow !== false) {
30✔
2647
                    $prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start);
27✔
2648
                    if ($prevComma === false) {
27✔
2649
                        // No comma between this token and the last match arrow,
2650
                        // so this token exists after the arrow and we can continue
2651
                        // checking as normal.
2652
                        $beforeArrow = false;
12✔
2653
                    }
4✔
2654
                }
9✔
2655

2656
                if ($beforeArrow === true) {
30✔
2657
                    $nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
30✔
2658
                    if ($nextMatchArrow !== false) {
30✔
2659
                        $start = $nextMatchArrow;
30✔
2660
                    }
10✔
2661
                }
10✔
2662
            }//end if
10✔
2663
        }//end if
22✔
2664

2665
        $lastNotEmpty = $start;
66✔
2666
        for ($i = $start; $i < $this->numTokens; $i++) {
66✔
2667
            if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
66✔
2668
                // Found the end of the statement.
2669
                if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS
60✔
2670
                    || $this->tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
58✔
2671
                    || $this->tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
57✔
2672
                    || $this->tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
53✔
2673
                    || $this->tokens[$i]['code'] === T_OPEN_TAG
49✔
2674
                    || $this->tokens[$i]['code'] === T_CLOSE_TAG
56✔
2675
                ) {
20✔
2676
                    return $lastNotEmpty;
24✔
2677
                }
2678

2679
                return $i;
48✔
2680
            }
2681

2682
            // Skip nested statements.
2683
            if (isset($this->tokens[$i]['scope_closer']) === true
66✔
2684
                && ($i === $this->tokens[$i]['scope_opener']
56✔
2685
                || $i === $this->tokens[$i]['scope_condition'])
56✔
2686
            ) {
22✔
2687
                if ($this->tokens[$i]['code'] === T_FN) {
36✔
2688
                    $lastNotEmpty = $this->tokens[$i]['scope_closer'];
18✔
2689
                    $i            = ($this->tokens[$i]['scope_closer'] - 1);
18✔
2690
                    continue;
18✔
2691
                }
2692

2693
                if ($i === $start && isset(Tokens::$scopeOpeners[$this->tokens[$i]['code']]) === true) {
21✔
2694
                    return $this->tokens[$i]['scope_closer'];
9✔
2695
                }
2696

2697
                $i = $this->tokens[$i]['scope_closer'];
15✔
2698
            } else if (isset($this->tokens[$i]['bracket_closer']) === true
48✔
2699
                && $i === $this->tokens[$i]['bracket_opener']
48✔
2700
            ) {
16✔
2701
                $i = $this->tokens[$i]['bracket_closer'];
6✔
2702
            } else if (isset($this->tokens[$i]['parenthesis_closer']) === true
48✔
2703
                && $i === $this->tokens[$i]['parenthesis_opener']
48✔
2704
            ) {
16✔
2705
                $i = $this->tokens[$i]['parenthesis_closer'];
9✔
2706
            } else if ($this->tokens[$i]['code'] === T_OPEN_USE_GROUP) {
48✔
2707
                $end = $this->findNext(T_CLOSE_USE_GROUP, ($i + 1));
6✔
2708
                if ($end !== false) {
6✔
2709
                    $i = $end;
6✔
2710
                }
2✔
2711
            }//end if
2✔
2712

2713
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
48✔
2714
                $lastNotEmpty = $i;
48✔
2715
            }
16✔
2716
        }//end for
16✔
2717

2718
        return ($this->numTokens - 1);
3✔
2719

2720
    }//end findEndOfStatement()
2721

2722

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

2749
        $foundToken = false;
×
2750

2751
        for ($i = $start; $i >= 0; $i--) {
×
2752
            if ($this->tokens[$i]['line'] < $this->tokens[$start]['line']) {
×
2753
                break;
×
2754
            }
2755

2756
            $found = $exclude;
×
2757
            foreach ($types as $type) {
×
2758
                if ($exclude === false) {
×
2759
                    if ($this->tokens[$i]['code'] === $type) {
×
2760
                        $found = true;
×
2761
                        break;
×
2762
                    }
2763
                } else {
2764
                    if ($this->tokens[$i]['code'] === $type) {
×
2765
                        $found = false;
×
2766
                        break;
×
2767
                    }
2768
                }
2769
            }
2770

2771
            if ($found === true) {
×
2772
                if ($value === null) {
×
2773
                    $foundToken = $i;
×
2774
                } else if ($this->tokens[$i]['content'] === $value) {
×
2775
                    $foundToken = $i;
×
2776
                }
2777
            }
2778
        }//end for
2779

2780
        return $foundToken;
×
2781

2782
    }//end findFirstOnLine()
2783

2784

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

2800
        // Make sure the token has conditions.
2801
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
18✔
2802
            return false;
3✔
2803
        }
2804

2805
        $types      = (array) $types;
15✔
2806
        $conditions = $this->tokens[$stackPtr]['conditions'];
15✔
2807

2808
        foreach ($types as $type) {
15✔
2809
            if (in_array($type, $conditions, true) === true) {
15✔
2810
                // We found a token with the required type.
2811
                return true;
15✔
2812
            }
2813
        }
5✔
2814

2815
        return false;
15✔
2816

2817
    }//end hasCondition()
2818

2819

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

2841
        // Make sure the token has conditions.
2842
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
27✔
2843
            return false;
3✔
2844
        }
2845

2846
        $conditions = $this->tokens[$stackPtr]['conditions'];
24✔
2847
        if ($first === false) {
24✔
2848
            $conditions = array_reverse($conditions, true);
12✔
2849
        }
4✔
2850

2851
        foreach ($conditions as $token => $condition) {
24✔
2852
            if ($condition === $type) {
24✔
2853
                return $token;
24✔
2854
            }
2855
        }
8✔
2856

2857
        return false;
24✔
2858

2859
    }//end getCondition()
2860

2861

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

2879
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
48✔
2880
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
48✔
2881
            && $this->tokens[$stackPtr]['code'] !== T_INTERFACE
48✔
2882
        ) {
16✔
2883
            return false;
3✔
2884
        }
2885

2886
        if (isset($this->tokens[$stackPtr]['scope_opener']) === false) {
45✔
2887
            return false;
3✔
2888
        }
2889

2890
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
42✔
2891
        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex);
42✔
2892
        if ($extendsIndex === false) {
42✔
2893
            return false;
9✔
2894
        }
2895

2896
        $find = [
11✔
2897
            T_NS_SEPARATOR,
33✔
2898
            T_STRING,
33✔
2899
            T_WHITESPACE,
33✔
2900
        ];
22✔
2901

2902
        $end  = $this->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true);
33✔
2903
        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
33✔
2904
        $name = trim($name);
33✔
2905

2906
        if ($name === '') {
33✔
2907
            return false;
3✔
2908
        }
2909

2910
        return $name;
30✔
2911

2912
    }//end findExtendedClassName()
2913

2914

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

2931
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
45✔
2932
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
45✔
2933
            && $this->tokens[$stackPtr]['code'] !== T_ENUM
45✔
2934
        ) {
15✔
2935
            return false;
6✔
2936
        }
2937

2938
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
39✔
2939
            return false;
3✔
2940
        }
2941

2942
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
36✔
2943
        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
36✔
2944
        if ($implementsIndex === false) {
36✔
2945
            return false;
6✔
2946
        }
2947

2948
        $find = [
10✔
2949
            T_NS_SEPARATOR,
30✔
2950
            T_STRING,
30✔
2951
            T_WHITESPACE,
30✔
2952
            T_COMMA,
30✔
2953
        ];
20✔
2954

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

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

2967
    }//end findImplementedInterfaceNames()
2968

2969

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

© 2026 Coveralls, Inc