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

PHPCSStandards / PHP_CodeSniffer / 8532137691

03 Apr 2024 02:02AM UTC coverage: 72.352% (+0.02%) from 72.328%
8532137691

push

github

jrfnl
File::getMethodProperties(): skip over closure use statements

This PR improves performance of the `File::getMethodProperties()` method and prevents incorrect return type information for closures `use` clauses containing invalid variable imports in the `use` clause (defensive coding).

Closure `use` statements can only import plain variables, not properties or other more complex variables.

As things were, when such "illegal" variables were imported in a closure `use`, the information for the return type could get mangled.
While this would be a parse error, for the purposes of static analysis, the `File::getMethodProperties()` method should still handle this correctly.

This commit updates the `File::getMethodProperties()` method to always skip over the complete `use` clause, which prevents the issue and improves performance as the same time (less token walking).

Includes unit tests.

6 of 7 new or added lines in 1 file covered. (85.71%)

4 existing lines in 3 files now uncovered.

17358 of 23991 relevant lines covered (72.35%)

55.3 hits per line

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

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

10
namespace PHP_CodeSniffer\Files;
11

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

21
class File
22
{
23

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

220

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

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

247
    }//end __construct()
248

249

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

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

271
    }//end setContent()
272

273

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

285
    }//end reloadContent()
×
286

287

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

297
    }//end disableCaching()
298

299

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

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

317
        $this->parse();
×
318

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

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

326
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
327
            Common::printStatusMessage('*** START TOKEN PROCESSING ***', 1);
×
328
        }
329

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

336
        // Foreach of the listeners that have registered to listen for this
337
        // token, get them to process it.
338
        foreach ($this->tokens as $stackPtr => $token) {
×
339
            // Check for ignored lines.
340
            if ($checkAnnotations === true
×
341
                && ($token['code'] === T_COMMENT
×
342
                || $token['code'] === T_PHPCS_IGNORE_FILE
×
343
                || $token['code'] === T_PHPCS_SET
×
344
                || $token['code'] === T_DOC_COMMENT_STRING
×
345
                || $token['code'] === T_DOC_COMMENT_TAG
×
346
                || ($inTests === true && $token['code'] === T_INLINE_HTML))
×
347
            ) {
348
                $commentText      = ltrim($this->tokens[$stackPtr]['content'], " \t/*#");
×
349
                $commentTextLower = strtolower($commentText);
×
350
                if (substr($commentTextLower, 0, 16) === 'phpcs:ignorefile'
×
351
                    || substr($commentTextLower, 0, 17) === '@phpcs:ignorefile'
×
352
                ) {
353
                    // Ignoring the whole file, just a little late.
354
                    $this->errors       = [];
×
355
                    $this->warnings     = [];
×
356
                    $this->errorCount   = 0;
×
357
                    $this->warningCount = 0;
×
358
                    $this->fixableCount = 0;
×
359
                    return;
×
360
                } else if (substr($commentTextLower, 0, 9) === 'phpcs:set'
×
361
                    || substr($commentTextLower, 0, 10) === '@phpcs:set'
×
362
                ) {
363
                    if (isset($token['sniffCode']) === true) {
×
364
                        $listenerCode = $token['sniffCode'];
×
365
                        if (isset($this->ruleset->sniffCodes[$listenerCode]) === true) {
×
366
                            $propertyCode  = $token['sniffProperty'];
×
367
                            $settings      = [
368
                                'value' => $token['sniffPropertyValue'],
×
369
                                'scope' => 'sniff',
×
370
                            ];
371
                            $listenerClass = $this->ruleset->sniffCodes[$listenerCode];
×
372
                            try {
373
                                $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $settings);
×
374
                            } catch (RuntimeException $e) {
×
375
                                // Non-existant property being set via an inline annotation.
376
                                // This is typically a PHPCS test case file, but we can't throw an error on the annotation
377
                                // line as it would get ignored. We also don't want this error to block
378
                                // the scan of the current file, so collect these and throw later.
379
                                $annotationErrors[] = 'Line '.$token['line'].': '.str_replace('Ruleset invalid. ', '', $e->getMessage());
×
380
                            }
381
                        }
382
                    }
383
                }//end if
384
            }//end if
385

386
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
387
                $type    = $token['type'];
×
388
                $content = Common::prepareForOutput($token['content']);
×
389
                Common::printStatusMessage("Process token $stackPtr: $type => $content", 2);
×
390
            }
391

392
            if ($token['code'] !== T_INLINE_HTML) {
×
393
                $foundCode = true;
×
394
            }
395

396
            if (isset($this->ruleset->tokenListeners[$token['code']]) === false) {
×
397
                continue;
×
398
            }
399

400
            foreach ($this->ruleset->tokenListeners[$token['code']] as $listenerData) {
×
401
                if (isset($this->ignoredListeners[$listenerData['class']]) === true
×
402
                    || (isset($listenerIgnoreTo[$listenerData['class']]) === true
×
403
                    && $listenerIgnoreTo[$listenerData['class']] > $stackPtr)
×
404
                ) {
405
                    // This sniff is ignoring past this token, or the whole file.
406
                    continue;
×
407
                }
408

409
                $class = $listenerData['class'];
×
410

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

423
                        $pattern = '`'.$pattern.'`i';
×
424
                        if (preg_match($pattern, $this->path) === 1) {
×
425
                            $this->ignoredListeners[$class] = true;
×
426
                            continue(2);
×
427
                        }
428
                    }
429

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

443
                            $pattern = '`'.$pattern.'`i';
×
444
                            if (preg_match($pattern, $this->path) === 1) {
×
445
                                $included = true;
×
446
                                break;
×
447
                            }
448
                        }
449

450
                        if ($included === false) {
×
451
                            $this->ignoredListeners[$class] = true;
×
452
                            continue;
×
453
                        }
454
                    }//end if
455
                }//end if
456

457
                $this->activeListener = $class;
×
458

459
                if ($this->configCache['trackTime'] === true) {
×
460
                    $startTime = microtime(true);
×
461
                }
462

463
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
464
                    Common::printStatusMessage('Processing '.$this->activeListener.'... ', 3, true);
×
465
                }
466

467
                $ignoreTo = $this->ruleset->sniffs[$class]->process($this, $stackPtr);
×
468
                if ($ignoreTo !== null) {
×
469
                    $listenerIgnoreTo[$this->activeListener] = $ignoreTo;
×
470
                }
471

472
                if ($this->configCache['trackTime'] === true) {
×
473
                    $timeTaken = (microtime(true) - $startTime);
×
474
                    if (isset($this->listenerTimes[$this->activeListener]) === false) {
×
475
                        $this->listenerTimes[$this->activeListener] = 0;
×
476
                    }
477

478
                    $this->listenerTimes[$this->activeListener] += $timeTaken;
×
479
                }
480

481
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
482
                    $timeTaken = round(($timeTaken), 4);
×
483
                    Common::printStatusMessage("DONE in $timeTaken seconds");
×
484
                }
485

486
                $this->activeListener = '';
×
487
            }//end foreach
488
        }//end foreach
489

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

504
        if ($annotationErrors !== []) {
×
505
            $error  = 'Encountered invalid inline phpcs:set annotations. Found:'.PHP_EOL;
×
506
            $error .= implode(PHP_EOL, $annotationErrors);
×
507

508
            $this->addWarning($error, null, 'Internal.PropertyDoesNotExist');
×
509
        }
510

511
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
512
            Common::printStatusMessage('*** END TOKEN PROCESSING ***', 1);
×
513
            Common::printStatusMessage('*** START SNIFF PROCESSING REPORT ***', 1);
×
514

515
            arsort($this->listenerTimes, SORT_NUMERIC);
×
516
            foreach ($this->listenerTimes as $listener => $timeTaken) {
×
517
                Common::printStatusMessage("$listener: ".round(($timeTaken), 4).' secs', 1);
×
518
            }
519

520
            Common::printStatusMessage('*** END SNIFF PROCESSING REPORT ***', 1);
×
521
        }
522

523
        $this->fixedCount += $this->fixer->getFixCount();
×
524

525
    }//end process()
526

527

528
    /**
529
     * Tokenizes the file and prepares it for the test run.
530
     *
531
     * @return void
532
     */
533
    public function parse()
×
534
    {
535
        if (empty($this->tokens) === false) {
×
536
            // File has already been parsed.
537
            return;
×
538
        }
539

540
        try {
541
            $this->tokenizer = new PHP($this->content, $this->config, $this->eolChar);
×
542
            $this->tokens    = $this->tokenizer->getTokens();
×
543
        } catch (TokenizerException $e) {
×
544
            $this->ignored = true;
×
545
            $this->addWarning($e->getMessage(), null, 'Internal.Tokenizer.Exception');
×
546
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
547
                Common::printStatusMessage('[tokenizer error]... ', 0, true);
×
548
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
549
                    Common::printStatusMessage(PHP_EOL, 0, true);
×
550
                }
551
            }
552

553
            return;
×
554
        }
555

556
        $this->numTokens = count($this->tokens);
×
557

558
        // Check for mixed line endings as these can cause tokenizer errors and we
559
        // should let the user know that the results they get may be incorrect.
560
        // This is done by removing all backslashes, removing the newline char we
561
        // detected, then converting newlines chars into text. If any backslashes
562
        // are left at the end, we have additional newline chars in use.
563
        $contents = str_replace('\\', '', $this->content);
×
564
        $contents = str_replace($this->eolChar, '', $contents);
×
565
        $contents = str_replace("\n", '\n', $contents);
×
566
        $contents = str_replace("\r", '\r', $contents);
×
567
        if (strpos($contents, '\\') !== false) {
×
568
            $error = 'File has mixed line endings; this may cause incorrect results';
×
569
            $this->addWarningOnLine($error, 1, 'Internal.LineEndings.Mixed');
×
570
        }
571

572
        if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
573
            if ($this->numTokens === 0) {
×
574
                $numLines = 0;
×
575
            } else {
576
                $numLines = $this->tokens[($this->numTokens - 1)]['line'];
×
577
            }
578

579
            Common::printStatusMessage("[$this->numTokens tokens in $numLines lines]... ", 0, true);
×
580
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
581
                Common::printStatusMessage(PHP_EOL, 0, true);
×
582
            }
583
        }
584

585
    }//end parse()
586

587

588
    /**
589
     * Returns the token stack for this file.
590
     *
591
     * @return array
592
     */
593
    public function getTokens()
×
594
    {
595
        return $this->tokens;
×
596

597
    }//end getTokens()
598

599

600
    /**
601
     * Remove vars stored in this file that are no longer required.
602
     *
603
     * @return void
604
     */
605
    public function cleanUp()
×
606
    {
607
        $this->listenerTimes = null;
×
608
        $this->content       = null;
×
609
        $this->tokens        = null;
×
610
        $this->metricTokens  = null;
×
611
        $this->tokenizer     = null;
×
612
        $this->fixer         = null;
×
613
        $this->config        = null;
×
614
        $this->ruleset       = null;
×
615

616
    }//end cleanUp()
617

618

619
    /**
620
     * Records an error against a specific token in the file.
621
     *
622
     * @param string  $error    The error message.
623
     * @param int     $stackPtr The stack position where the error occurred.
624
     * @param string  $code     A violation code unique to the sniff message.
625
     * @param array   $data     Replacements for the error message.
626
     * @param int     $severity The severity level for this error. A value of 0
627
     *                          will be converted into the default severity level.
628
     * @param boolean $fixable  Can the error be fixed by the sniff?
629
     *
630
     * @return boolean
631
     */
632
    public function addError(
×
633
        $error,
634
        $stackPtr,
635
        $code,
636
        $data=[],
637
        $severity=0,
638
        $fixable=false
639
    ) {
640
        if ($stackPtr === null) {
×
641
            $line   = 1;
×
642
            $column = 1;
×
643
        } else {
644
            $line   = $this->tokens[$stackPtr]['line'];
×
645
            $column = $this->tokens[$stackPtr]['column'];
×
646
        }
647

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

650
    }//end addError()
651

652

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

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

684
    }//end addWarning()
685

686

687
    /**
688
     * Records an error against a specific line in the file.
689
     *
690
     * @param string $error    The error message.
691
     * @param int    $line     The line on which the error occurred.
692
     * @param string $code     A violation code unique to the sniff message.
693
     * @param array  $data     Replacements for the error message.
694
     * @param int    $severity The severity level for this error. A value of 0
695
     *                         will be converted into the default severity level.
696
     *
697
     * @return boolean
698
     */
699
    public function addErrorOnLine(
×
700
        $error,
701
        $line,
702
        $code,
703
        $data=[],
704
        $severity=0
705
    ) {
706
        return $this->addMessage(true, $error, $line, 1, $code, $data, $severity, false);
×
707

708
    }//end addErrorOnLine()
709

710

711
    /**
712
     * Records a warning against a specific line in the file.
713
     *
714
     * @param string $warning  The error message.
715
     * @param int    $line     The line on which the warning occurred.
716
     * @param string $code     A violation code unique to the sniff message.
717
     * @param array  $data     Replacements for the warning message.
718
     * @param int    $severity The severity level for this warning. A value of 0 will
719
     *                         will be converted into the default severity level.
720
     *
721
     * @return boolean
722
     */
723
    public function addWarningOnLine(
×
724
        $warning,
725
        $line,
726
        $code,
727
        $data=[],
728
        $severity=0
729
    ) {
730
        return $this->addMessage(false, $warning, $line, 1, $code, $data, $severity, false);
×
731

732
    }//end addWarningOnLine()
733

734

735
    /**
736
     * Records a fixable error against a specific token in the file.
737
     *
738
     * Returns true if the error was recorded and should be fixed.
739
     *
740
     * @param string $error    The error message.
741
     * @param int    $stackPtr The stack position where the error occurred.
742
     * @param string $code     A violation code unique to the sniff message.
743
     * @param array  $data     Replacements for the error message.
744
     * @param int    $severity The severity level for this error. A value of 0
745
     *                         will be converted into the default severity level.
746
     *
747
     * @return boolean
748
     */
749
    public function addFixableError(
×
750
        $error,
751
        $stackPtr,
752
        $code,
753
        $data=[],
754
        $severity=0
755
    ) {
756
        $recorded = $this->addError($error, $stackPtr, $code, $data, $severity, true);
×
757
        if ($recorded === true && $this->fixer->enabled === true) {
×
758
            return true;
×
759
        }
760

761
        return false;
×
762

763
    }//end addFixableError()
764

765

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

792
        return false;
×
793

794
    }//end addFixableWarning()
795

796

797
    /**
798
     * Adds an error to the error stack.
799
     *
800
     * @param boolean $error    Is this an error message?
801
     * @param string  $message  The text of the message.
802
     * @param int     $line     The line on which the message occurred.
803
     * @param int     $column   The column at which the message occurred.
804
     * @param string  $code     A violation code unique to the sniff message.
805
     * @param array   $data     Replacements for the message.
806
     * @param int     $severity The severity level for this message. A value of 0
807
     *                          will be converted into the default severity level.
808
     * @param boolean $fixable  Can the problem be fixed by the sniff?
809
     *
810
     * @return boolean
811
     */
812
    protected function addMessage($error, $message, $line, $column, $code, $data, $severity, $fixable)
255✔
813
    {
814
        // Check if this line is ignoring all message codes.
815
        if (isset($this->tokenizer->ignoredLines[$line]['.all']) === true) {
255✔
816
            return false;
132✔
817
        }
818

819
        // Work out which sniff generated the message.
820
        $parts = explode('.', $code);
168✔
821
        if ($parts[0] === 'Internal') {
168✔
822
            // An internal message.
823
            $listenerCode = Common::getSniffCode($this->activeListener);
×
824
            $sniffCode    = $code;
×
825
            $checkCodes   = [$sniffCode];
×
826
        } else {
827
            if ($parts[0] !== $code) {
168✔
828
                // The full message code has been passed in.
829
                $sniffCode    = $code;
×
830
                $listenerCode = substr($sniffCode, 0, strrpos($sniffCode, '.'));
×
831
            } else {
832
                $listenerCode = Common::getSniffCode($this->activeListener);
168✔
833
                $sniffCode    = $listenerCode.'.'.$code;
168✔
834
                $parts        = explode('.', $sniffCode);
168✔
835
            }
836

837
            $checkCodes = [
112✔
838
                $sniffCode,
168✔
839
                $parts[0].'.'.$parts[1].'.'.$parts[2],
168✔
840
                $parts[0].'.'.$parts[1],
168✔
841
                $parts[0],
168✔
842
            ];
112✔
843
        }//end if
844

845
        if (isset($this->tokenizer->ignoredLines[$line]) === true) {
168✔
846
            // Check if this line is ignoring this specific message.
847
            $ignored = false;
99✔
848
            foreach ($checkCodes as $checkCode) {
99✔
849
                if (isset($this->tokenizer->ignoredLines[$line][$checkCode]) === true) {
99✔
850
                    $ignored = true;
90✔
851
                    break;
90✔
852
                }
853
            }
854

855
            // If it is ignored, make sure there is no exception in place.
856
            if ($ignored === true
99✔
857
                && isset($this->tokenizer->ignoredLines[$line]['.except']) === true
99✔
858
            ) {
859
                foreach ($checkCodes as $checkCode) {
15✔
860
                    if (isset($this->tokenizer->ignoredLines[$line]['.except'][$checkCode]) === true) {
15✔
861
                        $ignored = false;
12✔
862
                        break;
12✔
863
                    }
864
                }
865
            }
866

867
            if ($ignored === true) {
99✔
868
                return false;
90✔
869
            }
870
        }//end if
871

872
        $includeAll = true;
156✔
873
        if ($this->configCache['cache'] === false
156✔
874
            || $this->configCache['recordErrors'] === false
156✔
875
        ) {
876
            $includeAll = false;
156✔
877
        }
878

879
        // Filter out any messages for sniffs that shouldn't have run
880
        // due to the use of the --sniffs command line argument.
881
        if ($includeAll === false
156✔
882
            && ((empty($this->configCache['sniffs']) === false
156✔
883
            && in_array(strtolower($listenerCode), $this->configCache['sniffs'], true) === false)
156✔
884
            || (empty($this->configCache['exclude']) === false
156✔
885
            && in_array(strtolower($listenerCode), $this->configCache['exclude'], true) === true))
156✔
886
        ) {
887
            return false;
×
888
        }
889

890
        // If we know this sniff code is being ignored for this file, return early.
891
        foreach ($checkCodes as $checkCode) {
156✔
892
            if (isset($this->ignoredCodes[$checkCode]) === true) {
156✔
893
                return false;
×
894
            }
895
        }
896

897
        $oppositeType = 'warning';
156✔
898
        if ($error === false) {
156✔
899
            $oppositeType = 'error';
75✔
900
        }
901

902
        foreach ($checkCodes as $checkCode) {
156✔
903
            // Make sure this message type has not been set to the opposite message type.
904
            if (isset($this->ruleset->ruleset[$checkCode]['type']) === true
156✔
905
                && $this->ruleset->ruleset[$checkCode]['type'] === $oppositeType
156✔
906
            ) {
907
                $error = !$error;
×
908
                break;
×
909
            }
910
        }
911

912
        if ($error === true) {
156✔
913
            $configSeverity = $this->configCache['errorSeverity'];
132✔
914
            $messageCount   = &$this->errorCount;
132✔
915
            $messages       = &$this->errors;
132✔
916
        } else {
917
            $configSeverity = $this->configCache['warningSeverity'];
75✔
918
            $messageCount   = &$this->warningCount;
75✔
919
            $messages       = &$this->warnings;
75✔
920
        }
921

922
        if ($includeAll === false && $configSeverity === 0) {
156✔
923
            // Don't bother doing any processing as these messages are just going to
924
            // be hidden in the reports anyway.
925
            return false;
×
926
        }
927

928
        if ($severity === 0) {
156✔
929
            $severity = 5;
156✔
930
        }
931

932
        foreach ($checkCodes as $checkCode) {
156✔
933
            // Make sure we are interested in this severity level.
934
            if (isset($this->ruleset->ruleset[$checkCode]['severity']) === true) {
156✔
935
                $severity = $this->ruleset->ruleset[$checkCode]['severity'];
×
936
                break;
×
937
            }
938
        }
939

940
        if ($includeAll === false && $configSeverity > $severity) {
156✔
941
            return false;
×
942
        }
943

944
        // Make sure we are not ignoring this file.
945
        $included = null;
156✔
946
        if (trim($this->path, '\'"') === 'STDIN') {
156✔
947
            $included = true;
156✔
948
        } else {
949
            foreach ($checkCodes as $checkCode) {
×
950
                $patterns = null;
×
951

952
                if (isset($this->configCache['includePatterns'][$checkCode]) === true) {
×
953
                    $patterns  = $this->configCache['includePatterns'][$checkCode];
×
954
                    $excluding = false;
×
955
                } else if (isset($this->configCache['ignorePatterns'][$checkCode]) === true) {
×
956
                    $patterns  = $this->configCache['ignorePatterns'][$checkCode];
×
957
                    $excluding = true;
×
958
                }
959

960
                if ($patterns === null) {
×
961
                    continue;
×
962
                }
963

964
                foreach ($patterns as $pattern => $type) {
×
965
                    // While there is support for a type of each pattern
966
                    // (absolute or relative) we don't actually support it here.
967
                    $replacements = [
968
                        '\\,' => ',',
×
969
                        '*'   => '.*',
970
                    ];
971

972
                    // We assume a / directory separator, as do the exclude rules
973
                    // most developers write, so we need a special case for any system
974
                    // that is different.
975
                    if (DIRECTORY_SEPARATOR === '\\') {
×
976
                        $replacements['/'] = '\\\\';
×
977
                    }
978

979
                    $pattern = '`'.strtr($pattern, $replacements).'`i';
×
980
                    $matched = preg_match($pattern, $this->path);
×
981

982
                    if ($matched === 0) {
×
983
                        if ($excluding === false && $included === null) {
×
984
                            // This file path is not being included.
985
                            $included = false;
×
986
                        }
987

988
                        continue;
×
989
                    }
990

991
                    if ($excluding === true) {
×
992
                        // This file path is being excluded.
993
                        $this->ignoredCodes[$checkCode] = true;
×
994
                        return false;
×
995
                    }
996

997
                    // This file path is being included.
998
                    $included = true;
×
999
                    break;
×
1000
                }//end foreach
1001
            }//end foreach
1002
        }//end if
1003

1004
        if ($included === false) {
156✔
1005
            // There were include rules set, but this file
1006
            // path didn't match any of them.
1007
            return false;
×
1008
        }
1009

1010
        $messageCount++;
156✔
1011
        if ($fixable === true) {
156✔
1012
            $this->fixableCount++;
132✔
1013
        }
1014

1015
        if ($this->configCache['recordErrors'] === false
156✔
1016
            && $includeAll === false
156✔
1017
        ) {
1018
            return true;
×
1019
        }
1020

1021
        // See if there is a custom error message format to use.
1022
        // But don't do this if we are replaying errors because replayed
1023
        // errors have already used the custom format and have had their
1024
        // data replaced.
1025
        if ($this->replayingErrors === false
156✔
1026
            && isset($this->ruleset->ruleset[$sniffCode]['message']) === true
156✔
1027
        ) {
1028
            $message = $this->ruleset->ruleset[$sniffCode]['message'];
×
1029
        }
1030

1031
        if (empty($data) === false) {
156✔
1032
            $message = vsprintf($message, $data);
153✔
1033
        }
1034

1035
        if (isset($messages[$line]) === false) {
156✔
1036
            $messages[$line] = [];
156✔
1037
        }
1038

1039
        if (isset($messages[$line][$column]) === false) {
156✔
1040
            $messages[$line][$column] = [];
156✔
1041
        }
1042

1043
        $messages[$line][$column][] = [
156✔
1044
            'message'  => $message,
156✔
1045
            'source'   => $sniffCode,
156✔
1046
            'listener' => $this->activeListener,
156✔
1047
            'severity' => $severity,
156✔
1048
            'fixable'  => $fixable,
156✔
1049
        ];
104✔
1050

1051
        if (PHP_CODESNIFFER_VERBOSITY > 1
156✔
1052
            && $this->fixer->enabled === true
156✔
1053
            && $fixable === true
156✔
1054
        ) {
1055
            Common::forcePrintStatusMessage("E: [Line $line] $message ($sniffCode)", 1);
×
1056
        }
1057

1058
        return true;
156✔
1059

1060
    }//end addMessage()
1061

1062

1063
    /**
1064
     * Record a metric about the file being examined.
1065
     *
1066
     * @param int    $stackPtr The stack position where the metric was recorded.
1067
     * @param string $metric   The name of the metric being recorded.
1068
     * @param string $value    The value of the metric being recorded.
1069
     *
1070
     * @return boolean
1071
     */
1072
    public function recordMetric($stackPtr, $metric, $value)
×
1073
    {
1074
        if (isset($this->metrics[$metric]) === false) {
×
1075
            $this->metrics[$metric] = ['values' => [$value => 1]];
×
1076
            $this->metricTokens[$metric][$stackPtr] = true;
×
1077
        } else if (isset($this->metricTokens[$metric][$stackPtr]) === false) {
×
1078
            $this->metricTokens[$metric][$stackPtr] = true;
×
1079
            if (isset($this->metrics[$metric]['values'][$value]) === false) {
×
1080
                $this->metrics[$metric]['values'][$value] = 1;
×
1081
            } else {
1082
                $this->metrics[$metric]['values'][$value]++;
×
1083
            }
1084
        }
1085

1086
        return true;
×
1087

1088
    }//end recordMetric()
1089

1090

1091
    /**
1092
     * Returns the number of errors raised.
1093
     *
1094
     * @return int
1095
     */
1096
    public function getErrorCount()
×
1097
    {
1098
        return $this->errorCount;
×
1099

1100
    }//end getErrorCount()
1101

1102

1103
    /**
1104
     * Returns the number of warnings raised.
1105
     *
1106
     * @return int
1107
     */
1108
    public function getWarningCount()
×
1109
    {
1110
        return $this->warningCount;
×
1111

1112
    }//end getWarningCount()
1113

1114

1115
    /**
1116
     * Returns the number of fixable errors/warnings raised.
1117
     *
1118
     * @return int
1119
     */
1120
    public function getFixableCount()
×
1121
    {
1122
        return $this->fixableCount;
×
1123

1124
    }//end getFixableCount()
1125

1126

1127
    /**
1128
     * Returns the number of fixed errors/warnings.
1129
     *
1130
     * @return int
1131
     */
1132
    public function getFixedCount()
×
1133
    {
1134
        return $this->fixedCount;
×
1135

1136
    }//end getFixedCount()
1137

1138

1139
    /**
1140
     * Returns the list of ignored lines.
1141
     *
1142
     * @return array
1143
     */
1144
    public function getIgnoredLines()
×
1145
    {
1146
        return $this->tokenizer->ignoredLines;
×
1147

1148
    }//end getIgnoredLines()
1149

1150

1151
    /**
1152
     * Returns the errors raised from processing this file.
1153
     *
1154
     * @return array
1155
     */
1156
    public function getErrors()
×
1157
    {
1158
        return $this->errors;
×
1159

1160
    }//end getErrors()
1161

1162

1163
    /**
1164
     * Returns the warnings raised from processing this file.
1165
     *
1166
     * @return array
1167
     */
1168
    public function getWarnings()
×
1169
    {
1170
        return $this->warnings;
×
1171

1172
    }//end getWarnings()
1173

1174

1175
    /**
1176
     * Returns the metrics found while processing this file.
1177
     *
1178
     * @return array
1179
     */
1180
    public function getMetrics()
×
1181
    {
1182
        return $this->metrics;
×
1183

1184
    }//end getMetrics()
1185

1186

1187
    /**
1188
     * Returns the time taken processing this file for each invoked sniff.
1189
     *
1190
     * @return array
1191
     */
1192
    public function getListenerTimes()
×
1193
    {
1194
        return $this->listenerTimes;
×
1195

1196
    }//end getListenerTimes()
1197

1198

1199
    /**
1200
     * Returns the absolute filename of this file.
1201
     *
1202
     * @return string
1203
     */
1204
    public function getFilename()
×
1205
    {
1206
        return $this->path;
×
1207

1208
    }//end getFilename()
1209

1210

1211
    /**
1212
     * Returns the declaration name for classes, interfaces, traits, enums, and functions.
1213
     *
1214
     * @param int $stackPtr The position of the declaration token which
1215
     *                      declared the class, interface, trait, or function.
1216
     *
1217
     * @return string|null The name of the class, interface, trait, or function;
1218
     *                     or NULL if the function or class is anonymous.
1219
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type
1220
     *                                                      T_FUNCTION, T_CLASS, T_ANON_CLASS,
1221
     *                                                      T_CLOSURE, T_TRAIT, T_ENUM, or T_INTERFACE.
1222
     */
1223
    public function getDeclarationName($stackPtr)
84✔
1224
    {
1225
        $tokenCode = $this->tokens[$stackPtr]['code'];
84✔
1226

1227
        if ($tokenCode === T_ANON_CLASS || $tokenCode === T_CLOSURE) {
84✔
1228
            return null;
15✔
1229
        }
1230

1231
        if ($tokenCode !== T_FUNCTION
69✔
1232
            && $tokenCode !== T_CLASS
69✔
1233
            && $tokenCode !== T_INTERFACE
69✔
1234
            && $tokenCode !== T_TRAIT
69✔
1235
            && $tokenCode !== T_ENUM
69✔
1236
        ) {
1237
            throw new RuntimeException('Token type "'.$this->tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM');
3✔
1238
        }
1239

1240
        if ($tokenCode === T_FUNCTION
66✔
1241
            && strtolower($this->tokens[$stackPtr]['content']) !== 'function'
66✔
1242
        ) {
1243
            // This is a function declared without the "function" keyword.
1244
            // So this token is the function name.
1245
            return $this->tokens[$stackPtr]['content'];
×
1246
        }
1247

1248
        $content = null;
66✔
1249
        for ($i = $stackPtr; $i < $this->numTokens; $i++) {
66✔
1250
            if ($this->tokens[$i]['code'] === T_STRING) {
66✔
1251
                $content = $this->tokens[$i]['content'];
63✔
1252
                break;
63✔
1253
            }
1254
        }
1255

1256
        return $content;
66✔
1257

1258
    }//end getDeclarationName()
1259

1260

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

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

1328
        $opener = $this->tokens[$stackPtr]['parenthesis_opener'];
195✔
1329

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

1335
        $closer = $this->tokens[$opener]['parenthesis_closer'];
192✔
1336

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

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

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

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

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

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

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

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

1439
                    if ($nextEquals !== false) {
54✔
1440
                        break;
6✔
1441
                    }
1442
                }
1443

1444
                if ($defaultStart === null) {
123✔
1445
                    if ($typeHintToken === false) {
120✔
1446
                        $typeHintToken = $i;
117✔
1447
                    }
1448

1449
                    $typeHint        .= $this->tokens[$i]['content'];
120✔
1450
                    $typeHintEndToken = $i;
120✔
1451
                }
1452
                break;
123✔
1453
            case T_TYPE_UNION:
192✔
1454
            case T_TYPE_INTERSECTION:
192✔
1455
            case T_FALSE:
192✔
1456
            case T_TRUE:
192✔
1457
            case T_NULL:
192✔
1458
                // Part of a type hint or default value.
1459
                if ($defaultStart === null) {
81✔
1460
                    if ($typeHintToken === false) {
72✔
1461
                        $typeHintToken = $i;
15✔
1462
                    }
1463

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

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

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

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

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

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

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

1532
                if ($this->tokens[$i]['code'] === T_COMMA) {
183✔
1533
                    $vars[$paramCount]['comma_token'] = $i;
90✔
1534
                } else {
1535
                    $vars[$paramCount]['comma_token'] = false;
159✔
1536
                }
1537

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

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

1564
        return $vars;
192✔
1565

1566
    }//end getMethodParameters()
1567

1568

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

1607
        if ($this->tokens[$stackPtr]['code'] === T_FUNCTION) {
153✔
1608
            $valid = [
76✔
1609
                T_PUBLIC      => T_PUBLIC,
114✔
1610
                T_PRIVATE     => T_PRIVATE,
114✔
1611
                T_PROTECTED   => T_PROTECTED,
114✔
1612
                T_STATIC      => T_STATIC,
114✔
1613
                T_FINAL       => T_FINAL,
114✔
1614
                T_ABSTRACT    => T_ABSTRACT,
114✔
1615
                T_WHITESPACE  => T_WHITESPACE,
114✔
1616
                T_COMMENT     => T_COMMENT,
114✔
1617
                T_DOC_COMMENT => T_DOC_COMMENT,
114✔
1618
            ];
76✔
1619
        } else {
1620
            $valid = [
26✔
1621
                T_STATIC      => T_STATIC,
39✔
1622
                T_WHITESPACE  => T_WHITESPACE,
39✔
1623
                T_COMMENT     => T_COMMENT,
39✔
1624
                T_DOC_COMMENT => T_DOC_COMMENT,
39✔
1625
            ];
26✔
1626
        }
1627

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

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

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

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

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

1676
            $valid = [
102✔
1677
                T_STRING               => T_STRING,
153✔
1678
                T_NAME_QUALIFIED       => T_NAME_QUALIFIED,
153✔
1679
                T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED,
153✔
1680
                T_NAME_RELATIVE        => T_NAME_RELATIVE,
153✔
1681
                T_CALLABLE             => T_CALLABLE,
153✔
1682
                T_SELF                 => T_SELF,
153✔
1683
                T_PARENT               => T_PARENT,
153✔
1684
                T_STATIC               => T_STATIC,
153✔
1685
                T_FALSE                => T_FALSE,
153✔
1686
                T_TRUE                 => T_TRUE,
153✔
1687
                T_NULL                 => T_NULL,
153✔
1688
                T_TYPE_INTERSECTION    => T_TYPE_INTERSECTION,
153✔
1689
                T_TYPE_UNION           => T_TYPE_UNION,
153✔
1690
            ];
102✔
1691

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

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

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

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

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

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

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

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

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

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

1755
    }//end getMethodProperties()
1756

1757

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

1793
        $conditions = array_keys($this->tokens[$stackPtr]['conditions']);
258✔
1794
        $ptr        = array_pop($conditions);
258✔
1795
        if (isset($this->tokens[$ptr]) === false
258✔
1796
            || ($this->tokens[$ptr]['code'] !== T_CLASS
257✔
1797
            && $this->tokens[$ptr]['code'] !== T_ANON_CLASS
257✔
1798
            && $this->tokens[$ptr]['code'] !== T_TRAIT)
258✔
1799
        ) {
1800
            if (isset($this->tokens[$ptr]) === true
18✔
1801
                && ($this->tokens[$ptr]['code'] === T_INTERFACE
17✔
1802
                || $this->tokens[$ptr]['code'] === T_ENUM)
18✔
1803
            ) {
1804
                // T_VARIABLEs in interfaces/enums can actually be method arguments
1805
                // but they won't be seen as being inside the method because there
1806
                // are no scope openers and closers for abstract methods. If it is in
1807
                // parentheses, we can be pretty sure it is a method argument.
1808
                if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === false
9✔
1809
                    || empty($this->tokens[$stackPtr]['nested_parenthesis']) === true
9✔
1810
                ) {
1811
                    // Parse error or live coding.
1812
                    return [];
8✔
1813
                }
1814
            } else {
1815
                throw new RuntimeException('$stackPtr is not a class member var');
9✔
1816
            }
1817
        }//end if
1818

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

1831
        $valid = [
154✔
1832
            T_PUBLIC    => T_PUBLIC,
231✔
1833
            T_PRIVATE   => T_PRIVATE,
231✔
1834
            T_PROTECTED => T_PROTECTED,
231✔
1835
            T_STATIC    => T_STATIC,
231✔
1836
            T_VAR       => T_VAR,
231✔
1837
            T_READONLY  => T_READONLY,
231✔
1838
        ];
154✔
1839

1840
        $valid += Tokens::$emptyTokens;
231✔
1841

1842
        $scope          = 'public';
231✔
1843
        $scopeSpecified = false;
231✔
1844
        $isStatic       = false;
231✔
1845
        $isReadonly     = false;
231✔
1846

1847
        $startOfStatement = $this->findPrevious(
231✔
1848
            [
154✔
1849
                T_SEMICOLON,
231✔
1850
                T_OPEN_CURLY_BRACKET,
231✔
1851
                T_CLOSE_CURLY_BRACKET,
231✔
1852
                T_ATTRIBUTE_END,
231✔
1853
            ],
154✔
1854
            ($stackPtr - 1)
231✔
1855
        );
154✔
1856

1857
        for ($i = ($startOfStatement + 1); $i < $stackPtr; $i++) {
231✔
1858
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
231✔
1859
                break;
171✔
1860
            }
1861

1862
            switch ($this->tokens[$i]['code']) {
231✔
1863
            case T_PUBLIC:
231✔
1864
                $scope          = 'public';
108✔
1865
                $scopeSpecified = true;
108✔
1866
                break;
108✔
1867
            case T_PRIVATE:
231✔
1868
                $scope          = 'private';
54✔
1869
                $scopeSpecified = true;
54✔
1870
                break;
54✔
1871
            case T_PROTECTED:
231✔
1872
                $scope          = 'protected';
36✔
1873
                $scopeSpecified = true;
36✔
1874
                break;
36✔
1875
            case T_STATIC:
231✔
1876
                $isStatic = true;
60✔
1877
                break;
60✔
1878
            case T_READONLY:
231✔
1879
                $isReadonly = true;
27✔
1880
                break;
27✔
1881
            }
1882
        }//end for
1883

1884
        $type         = '';
231✔
1885
        $typeToken    = false;
231✔
1886
        $typeEndToken = false;
231✔
1887
        $nullableType = false;
231✔
1888

1889
        if ($i < $stackPtr) {
231✔
1890
            // We've found a type.
1891
            $valid = [
114✔
1892
                T_STRING               => T_STRING,
171✔
1893
                T_NAME_QUALIFIED       => T_NAME_QUALIFIED,
171✔
1894
                T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED,
171✔
1895
                T_NAME_RELATIVE        => T_NAME_RELATIVE,
171✔
1896
                T_CALLABLE             => T_CALLABLE,
171✔
1897
                T_SELF                 => T_SELF,
171✔
1898
                T_PARENT               => T_PARENT,
171✔
1899
                T_FALSE                => T_FALSE,
171✔
1900
                T_TRUE                 => T_TRUE,
171✔
1901
                T_NULL                 => T_NULL,
171✔
1902
                T_TYPE_INTERSECTION    => T_TYPE_INTERSECTION,
171✔
1903
                T_TYPE_UNION           => T_TYPE_UNION,
171✔
1904
            ];
114✔
1905

1906
            for ($i; $i < $stackPtr; $i++) {
171✔
1907
                if ($this->tokens[$i]['code'] === T_VARIABLE) {
171✔
1908
                    // Hit another variable in a group definition.
1909
                    break;
30✔
1910
                }
1911

1912
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
147✔
1913
                    $nullableType = true;
45✔
1914
                }
1915

1916
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
147✔
1917
                    $typeEndToken = $i;
147✔
1918
                    if ($typeToken === false) {
147✔
1919
                        $typeToken = $i;
147✔
1920
                    }
1921

1922
                    $type .= $this->tokens[$i]['content'];
147✔
1923
                }
1924
            }
1925

1926
            if ($type !== '' && $nullableType === true) {
171✔
1927
                $type = '?'.$type;
45✔
1928
            }
1929
        }//end if
1930

1931
        return [
154✔
1932
            'scope'           => $scope,
231✔
1933
            'scope_specified' => $scopeSpecified,
231✔
1934
            'is_static'       => $isStatic,
231✔
1935
            'is_readonly'     => $isReadonly,
231✔
1936
            'type'            => $type,
231✔
1937
            'type_token'      => $typeToken,
231✔
1938
            'type_end_token'  => $typeEndToken,
231✔
1939
            'nullable_type'   => $nullableType,
231✔
1940
        ];
154✔
1941

1942
    }//end getMemberProperties()
1943

1944

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

1970
        $valid = [
22✔
1971
            T_FINAL       => T_FINAL,
33✔
1972
            T_ABSTRACT    => T_ABSTRACT,
33✔
1973
            T_READONLY    => T_READONLY,
33✔
1974
            T_WHITESPACE  => T_WHITESPACE,
33✔
1975
            T_COMMENT     => T_COMMENT,
33✔
1976
            T_DOC_COMMENT => T_DOC_COMMENT,
33✔
1977
        ];
22✔
1978

1979
        $isAbstract = false;
33✔
1980
        $isFinal    = false;
33✔
1981
        $isReadonly = false;
33✔
1982

1983
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
33✔
1984
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
33✔
1985
                break;
33✔
1986
            }
1987

1988
            switch ($this->tokens[$i]['code']) {
33✔
1989
            case T_ABSTRACT:
33✔
1990
                $isAbstract = true;
15✔
1991
                break;
15✔
1992

1993
            case T_FINAL:
33✔
1994
                $isFinal = true;
12✔
1995
                break;
12✔
1996

1997
            case T_READONLY:
33✔
1998
                $isReadonly = true;
15✔
1999
                break;
15✔
2000
            }
2001
        }//end for
2002

2003
        return [
22✔
2004
            'is_abstract' => $isAbstract,
33✔
2005
            'is_final'    => $isFinal,
33✔
2006
            'is_readonly' => $isReadonly,
33✔
2007
        ];
22✔
2008

2009
    }//end getClassProperties()
2010

2011

2012
    /**
2013
     * Determine if the passed token is a reference operator.
2014
     *
2015
     * Returns true if the specified token position represents a reference.
2016
     * Returns false if the token represents a bitwise operator.
2017
     *
2018
     * @param int $stackPtr The position of the T_BITWISE_AND token.
2019
     *
2020
     * @return boolean
2021
     */
2022
    public function isReference($stackPtr)
219✔
2023
    {
2024
        if ($this->tokens[$stackPtr]['code'] !== T_BITWISE_AND) {
219✔
2025
            return false;
3✔
2026
        }
2027

2028
        $tokenBefore = $this->findPrevious(
216✔
2029
            Tokens::$emptyTokens,
216✔
2030
            ($stackPtr - 1),
216✔
2031
            null,
216✔
2032
            true
216✔
2033
        );
144✔
2034

2035
        if ($this->tokens[$tokenBefore]['code'] === T_FUNCTION
216✔
2036
            || $this->tokens[$tokenBefore]['code'] === T_CLOSURE
213✔
2037
            || $this->tokens[$tokenBefore]['code'] === T_FN
216✔
2038
        ) {
2039
            // Function returns a reference.
2040
            return true;
9✔
2041
        }
2042

2043
        if ($this->tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
207✔
2044
            // Inside a foreach loop or array assignment, this is a reference.
2045
            return true;
18✔
2046
        }
2047

2048
        if ($this->tokens[$tokenBefore]['code'] === T_AS) {
189✔
2049
            // Inside a foreach loop, this is a reference.
2050
            return true;
3✔
2051
        }
2052

2053
        if (isset(Tokens::$assignmentTokens[$this->tokens[$tokenBefore]['code']]) === true) {
186✔
2054
            // This is directly after an assignment. It's a reference. Even if
2055
            // it is part of an operation, the other tests will handle it.
2056
            return true;
21✔
2057
        }
2058

2059
        $tokenAfter = $this->findNext(
165✔
2060
            Tokens::$emptyTokens,
165✔
2061
            ($stackPtr + 1),
165✔
2062
            null,
165✔
2063
            true
165✔
2064
        );
110✔
2065

2066
        if ($this->tokens[$tokenAfter]['code'] === T_NEW) {
165✔
2067
            return true;
3✔
2068
        }
2069

2070
        if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === true) {
162✔
2071
            $brackets    = $this->tokens[$stackPtr]['nested_parenthesis'];
114✔
2072
            $lastBracket = array_pop($brackets);
114✔
2073
            if (isset($this->tokens[$lastBracket]['parenthesis_owner']) === true) {
114✔
2074
                $owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']];
78✔
2075
                if ($owner['code'] === T_FUNCTION
78✔
2076
                    || $owner['code'] === T_CLOSURE
66✔
2077
                    || $owner['code'] === T_FN
78✔
2078
                ) {
2079
                    $params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']);
48✔
2080
                    foreach ($params as $param) {
68✔
2081
                        if ($param['reference_token'] === $stackPtr) {
48✔
2082
                            // Function parameter declared to be passed by reference.
2083
                            return true;
33✔
2084
                        }
2085
                    }
2086
                }//end if
2087
            } else {
2088
                $prev = false;
36✔
2089
                for ($t = ($this->tokens[$lastBracket]['parenthesis_opener'] - 1); $t >= 0; $t--) {
36✔
2090
                    if ($this->tokens[$t]['code'] !== T_WHITESPACE) {
36✔
2091
                        $prev = $t;
36✔
2092
                        break;
36✔
2093
                    }
2094
                }
2095

2096
                if ($prev !== false && $this->tokens[$prev]['code'] === T_USE) {
36✔
2097
                    // Closure use by reference.
2098
                    return true;
×
2099
                }
2100
            }//end if
2101
        }//end if
2102

2103
        // Pass by reference in function calls and assign by reference in arrays.
2104
        if ($this->tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
129✔
2105
            || $this->tokens[$tokenBefore]['code'] === T_COMMA
111✔
2106
            || $this->tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY
129✔
2107
        ) {
2108
            if ($this->tokens[$tokenAfter]['code'] === T_VARIABLE) {
93✔
2109
                return true;
69✔
2110
            } else {
2111
                $skip   = Tokens::$emptyTokens;
24✔
2112
                $skip[] = T_SELF;
24✔
2113
                $skip[] = T_PARENT;
24✔
2114
                $skip[] = T_STATIC;
24✔
2115
                $skip[] = T_STRING;
24✔
2116
                $skip[] = T_NAME_QUALIFIED;
24✔
2117
                $skip[] = T_NAME_FULLY_QUALIFIED;
24✔
2118
                $skip[] = T_NAME_RELATIVE;
24✔
2119
                $skip[] = T_DOUBLE_COLON;
24✔
2120

2121
                $nextSignificantAfter = $this->findNext(
24✔
2122
                    $skip,
24✔
2123
                    ($stackPtr + 1),
24✔
2124
                    null,
24✔
2125
                    true
24✔
2126
                );
16✔
2127
                if ($this->tokens[$nextSignificantAfter]['code'] === T_VARIABLE) {
24✔
2128
                    return true;
24✔
2129
                }
2130
            }//end if
2131
        }//end if
2132

2133
        return false;
36✔
2134

2135
    }//end isReference()
2136

2137

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

2156
        if (is_int($length) === false || $length <= 0) {
78✔
2157
            return '';
9✔
2158
        }
2159

2160
        $str = '';
69✔
2161
        $end = ($start + $length);
69✔
2162
        if ($end > $this->numTokens) {
69✔
2163
            $end = $this->numTokens;
3✔
2164
        }
2165

2166
        for ($i = $start; $i < $end; $i++) {
69✔
2167
            // If tabs are being converted to spaces by the tokeniser, the
2168
            // original content should be used instead of the converted content.
2169
            if ($origContent === true && isset($this->tokens[$i]['orig_content']) === true) {
69✔
2170
                $str .= $this->tokens[$i]['orig_content'];
6✔
2171
            } else {
2172
                $str .= $this->tokens[$i]['content'];
69✔
2173
            }
2174
        }
2175

2176
        return $str;
69✔
2177

2178
    }//end getTokensAsString()
2179

2180

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

2217
        if ($end === null) {
×
2218
            $end = 0;
×
2219
        }
2220

2221
        for ($i = $start; $i >= $end; $i--) {
×
2222
            $found = (bool) $exclude;
×
2223
            foreach ($types as $type) {
×
2224
                if ($this->tokens[$i]['code'] === $type) {
×
2225
                    $found = !$exclude;
×
2226
                    break;
×
2227
                }
2228
            }
2229

2230
            if ($found === true) {
×
2231
                if ($value === null) {
×
2232
                    return $i;
×
2233
                } else if ($this->tokens[$i]['content'] === $value) {
×
2234
                    return $i;
×
2235
                }
2236
            }
2237

2238
            if ($local === true) {
×
2239
                if (isset($this->tokens[$i]['scope_opener']) === true
×
2240
                    && $i === $this->tokens[$i]['scope_closer']
×
2241
                ) {
2242
                    $i = $this->tokens[$i]['scope_opener'];
×
2243
                } else if (isset($this->tokens[$i]['bracket_opener']) === true
×
2244
                    && $i === $this->tokens[$i]['bracket_closer']
×
2245
                ) {
2246
                    $i = $this->tokens[$i]['bracket_opener'];
×
2247
                } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
×
2248
                    && $i === $this->tokens[$i]['parenthesis_closer']
×
2249
                ) {
2250
                    $i = $this->tokens[$i]['parenthesis_opener'];
×
2251
                } else if ($this->tokens[$i]['code'] === T_SEMICOLON) {
×
2252
                    break;
×
2253
                }
2254
            }
2255
        }//end for
2256

2257
        return false;
×
2258

2259
    }//end findPrevious()
2260

2261

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

2298
        if ($end === null || $end > $this->numTokens) {
×
2299
            $end = $this->numTokens;
×
2300
        }
2301

2302
        for ($i = $start; $i < $end; $i++) {
×
2303
            $found = (bool) $exclude;
×
2304
            foreach ($types as $type) {
×
2305
                if ($this->tokens[$i]['code'] === $type) {
×
2306
                    $found = !$exclude;
×
2307
                    break;
×
2308
                }
2309
            }
2310

2311
            if ($found === true) {
×
2312
                if ($value === null) {
×
2313
                    return $i;
×
2314
                } else if ($this->tokens[$i]['content'] === $value) {
×
2315
                    return $i;
×
2316
                }
2317
            }
2318

2319
            if ($local === true && $this->tokens[$i]['code'] === T_SEMICOLON) {
×
2320
                break;
×
2321
            }
2322
        }//end for
2323

2324
        return false;
×
2325

2326
    }//end findNext()
2327

2328

2329
    /**
2330
     * Returns the position of the first non-whitespace token in a statement.
2331
     *
2332
     * @param int              $start  The position to start searching from in the token stack.
2333
     * @param int|string|array $ignore Token types that should not be considered stop points.
2334
     *
2335
     * @return int
2336
     */
2337
    public function findStartOfStatement($start, $ignore=null)
120✔
2338
    {
2339
        $startTokens = Tokens::$blockOpeners;
120✔
2340
        $startTokens[T_OPEN_SHORT_ARRAY]   = true;
120✔
2341
        $startTokens[T_OPEN_TAG]           = true;
120✔
2342
        $startTokens[T_OPEN_TAG_WITH_ECHO] = true;
120✔
2343

2344
        $endTokens = [
80✔
2345
            T_CLOSE_TAG    => true,
120✔
2346
            T_COLON        => true,
120✔
2347
            T_COMMA        => true,
120✔
2348
            T_DOUBLE_ARROW => true,
120✔
2349
            T_MATCH_ARROW  => true,
120✔
2350
            T_SEMICOLON    => true,
120✔
2351
        ];
80✔
2352

2353
        if ($ignore !== null) {
120✔
2354
            $ignore = (array) $ignore;
×
2355
            foreach ($ignore as $code) {
×
2356
                if (isset($startTokens[$code]) === true) {
×
2357
                    unset($startTokens[$code]);
×
2358
                }
2359

2360
                if (isset($endTokens[$code]) === true) {
×
2361
                    unset($endTokens[$code]);
×
2362
                }
2363
            }
2364
        }
2365

2366
        // If the start token is inside the case part of a match expression,
2367
        // find the start of the condition. If it's in the statement part, find
2368
        // the token that comes after the match arrow.
2369
        $matchExpression = $this->getCondition($start, T_MATCH);
120✔
2370
        if ($matchExpression !== false) {
120✔
2371
            for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
27✔
2372
                if ($prevMatch !== $start
27✔
2373
                    && ($this->tokens[$prevMatch]['code'] === T_MATCH_ARROW
27✔
2374
                    || $this->tokens[$prevMatch]['code'] === T_COMMA)
27✔
2375
                ) {
2376
                    break;
27✔
2377
                }
2378

2379
                // Skip nested statements.
2380
                if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
27✔
2381
                    && $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
27✔
2382
                ) {
2383
                    $prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
9✔
2384
                } else if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
27✔
2385
                    && $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
27✔
2386
                ) {
2387
                    $prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
12✔
2388
                }
2389
            }
2390

2391
            if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) {
27✔
2392
                // We're before the arrow in the first case.
2393
                $next = $this->findNext(Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
6✔
2394
                if ($next === false) {
6✔
2395
                    return $start;
×
2396
                }
2397

2398
                return $next;
6✔
2399
            }
2400

2401
            if ($this->tokens[$prevMatch]['code'] === T_COMMA) {
27✔
2402
                // We're before the arrow, but not in the first case.
2403
                $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($prevMatch - 1), $this->tokens[$matchExpression]['scope_opener']);
15✔
2404
                if ($prevMatchArrow === false) {
15✔
2405
                    // We're before the arrow in the first case.
2406
                    $next = $this->findNext(Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
3✔
2407
                    return $next;
3✔
2408
                }
2409

2410
                $end  = $this->findEndOfStatement($prevMatchArrow);
12✔
2411
                $next = $this->findNext(Tokens::$emptyTokens, ($end + 1), null, true);
12✔
2412
                return $next;
12✔
2413
            }
2414
        }//end if
2415

2416
        $lastNotEmpty = $start;
117✔
2417

2418
        // If we are starting at a token that ends a scope block, skip to
2419
        // the start and continue from there.
2420
        // If we are starting at a token that ends a statement, skip this
2421
        // token so we find the true start of the statement.
2422
        while (isset($endTokens[$this->tokens[$start]['code']]) === true
117✔
2423
            || (isset($this->tokens[$start]['scope_condition']) === true
117✔
2424
            && $start === $this->tokens[$start]['scope_closer'])
117✔
2425
        ) {
2426
            if (isset($this->tokens[$start]['scope_condition']) === true) {
51✔
2427
                $start = $this->tokens[$start]['scope_condition'];
30✔
2428
            } else {
2429
                $start--;
30✔
2430
            }
2431
        }
2432

2433
        for ($i = $start; $i >= 0; $i--) {
117✔
2434
            if (isset($startTokens[$this->tokens[$i]['code']]) === true
117✔
2435
                || isset($endTokens[$this->tokens[$i]['code']]) === true
117✔
2436
            ) {
2437
                // Found the end of the previous statement.
2438
                return $lastNotEmpty;
117✔
2439
            }
2440

2441
            if (isset($this->tokens[$i]['scope_opener']) === true
117✔
2442
                && $i === $this->tokens[$i]['scope_closer']
117✔
2443
                && $this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS
117✔
2444
                && $this->tokens[$i]['code'] !== T_END_NOWDOC
117✔
2445
                && $this->tokens[$i]['code'] !== T_END_HEREDOC
117✔
2446
                && $this->tokens[$i]['code'] !== T_BREAK
117✔
2447
                && $this->tokens[$i]['code'] !== T_RETURN
117✔
2448
                && $this->tokens[$i]['code'] !== T_CONTINUE
117✔
2449
                && $this->tokens[$i]['code'] !== T_THROW
117✔
2450
                && $this->tokens[$i]['code'] !== T_EXIT
117✔
2451
            ) {
2452
                // Found the end of the previous scope block.
2453
                return $lastNotEmpty;
×
2454
            }
2455

2456
            // Skip nested statements.
2457
            if (isset($this->tokens[$i]['bracket_opener']) === true
117✔
2458
                && $i === $this->tokens[$i]['bracket_closer']
117✔
2459
            ) {
2460
                $i = $this->tokens[$i]['bracket_opener'];
3✔
2461
            } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
117✔
2462
                && $i === $this->tokens[$i]['parenthesis_closer']
117✔
2463
            ) {
2464
                $i = $this->tokens[$i]['parenthesis_opener'];
21✔
2465
            } else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
117✔
2466
                $start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
3✔
2467
                if ($start !== false) {
3✔
2468
                    $i = $start;
3✔
2469
                }
2470
            }//end if
2471

2472
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
117✔
2473
                $lastNotEmpty = $i;
117✔
2474
            }
2475
        }//end for
2476

2477
        return 0;
×
2478

2479
    }//end findStartOfStatement()
2480

2481

2482
    /**
2483
     * Returns the position of the last non-whitespace token in a statement.
2484
     *
2485
     * @param int              $start  The position to start searching from in the token stack.
2486
     * @param int|string|array $ignore Token types that should not be considered stop points.
2487
     *
2488
     * @return int
2489
     */
2490
    public function findEndOfStatement($start, $ignore=null)
63✔
2491
    {
2492
        $endTokens = [
42✔
2493
            T_COLON                => true,
63✔
2494
            T_COMMA                => true,
63✔
2495
            T_DOUBLE_ARROW         => true,
63✔
2496
            T_SEMICOLON            => true,
63✔
2497
            T_CLOSE_PARENTHESIS    => true,
63✔
2498
            T_CLOSE_SQUARE_BRACKET => true,
63✔
2499
            T_CLOSE_CURLY_BRACKET  => true,
63✔
2500
            T_CLOSE_SHORT_ARRAY    => true,
63✔
2501
            T_OPEN_TAG             => true,
63✔
2502
            T_CLOSE_TAG            => true,
63✔
2503
        ];
42✔
2504

2505
        if ($ignore !== null) {
63✔
2506
            $ignore = (array) $ignore;
×
2507
            foreach ($ignore as $code) {
×
2508
                unset($endTokens[$code]);
×
2509
            }
2510
        }
2511

2512
        // If the start token is inside the case part of a match expression,
2513
        // advance to the match arrow and continue looking for the
2514
        // end of the statement from there so that we skip over commas.
2515
        if ($this->tokens[$start]['code'] !== T_MATCH_ARROW) {
63✔
2516
            $matchExpression = $this->getCondition($start, T_MATCH);
63✔
2517
            if ($matchExpression !== false) {
63✔
2518
                $beforeArrow    = true;
27✔
2519
                $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']);
27✔
2520
                if ($prevMatchArrow !== false) {
27✔
2521
                    $prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start);
24✔
2522
                    if ($prevComma === false) {
24✔
2523
                        // No comma between this token and the last match arrow,
2524
                        // so this token exists after the arrow and we can continue
2525
                        // checking as normal.
2526
                        $beforeArrow = false;
9✔
2527
                    }
2528
                }
2529

2530
                if ($beforeArrow === true) {
27✔
2531
                    $nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
27✔
2532
                    if ($nextMatchArrow !== false) {
27✔
2533
                        $start = $nextMatchArrow;
27✔
2534
                    }
2535
                }
2536
            }//end if
2537
        }//end if
2538

2539
        $lastNotEmpty = $start;
63✔
2540
        for ($i = $start; $i < $this->numTokens; $i++) {
63✔
2541
            if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
63✔
2542
                // Found the end of the statement.
2543
                if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS
57✔
2544
                    || $this->tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
54✔
2545
                    || $this->tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
54✔
2546
                    || $this->tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
48✔
2547
                    || $this->tokens[$i]['code'] === T_OPEN_TAG
45✔
2548
                    || $this->tokens[$i]['code'] === T_CLOSE_TAG
57✔
2549
                ) {
2550
                    return $lastNotEmpty;
21✔
2551
                }
2552

2553
                return $i;
45✔
2554
            }
2555

2556
            // Skip nested statements.
2557
            if (isset($this->tokens[$i]['scope_closer']) === true
63✔
2558
                && ($i === $this->tokens[$i]['scope_opener']
53✔
2559
                || $i === $this->tokens[$i]['scope_condition'])
63✔
2560
            ) {
2561
                if ($this->tokens[$i]['code'] === T_FN) {
33✔
2562
                    $lastNotEmpty = $this->tokens[$i]['scope_closer'];
15✔
2563
                    $i            = ($this->tokens[$i]['scope_closer'] - 1);
15✔
2564
                    continue;
15✔
2565
                }
2566

2567
                if ($i === $start && isset(Tokens::$scopeOpeners[$this->tokens[$i]['code']]) === true) {
18✔
2568
                    return $this->tokens[$i]['scope_closer'];
6✔
2569
                }
2570

2571
                $i = $this->tokens[$i]['scope_closer'];
12✔
2572
            } else if (isset($this->tokens[$i]['bracket_closer']) === true
45✔
2573
                && $i === $this->tokens[$i]['bracket_opener']
45✔
2574
            ) {
2575
                $i = $this->tokens[$i]['bracket_closer'];
3✔
2576
            } else if (isset($this->tokens[$i]['parenthesis_closer']) === true
45✔
2577
                && $i === $this->tokens[$i]['parenthesis_opener']
45✔
2578
            ) {
2579
                $i = $this->tokens[$i]['parenthesis_closer'];
6✔
2580
            } else if ($this->tokens[$i]['code'] === T_OPEN_USE_GROUP) {
45✔
2581
                $end = $this->findNext(T_CLOSE_USE_GROUP, ($i + 1));
3✔
2582
                if ($end !== false) {
3✔
2583
                    $i = $end;
3✔
2584
                }
2585
            }//end if
2586

2587
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
45✔
2588
                $lastNotEmpty = $i;
45✔
2589
            }
2590
        }//end for
2591

2592
        return ($this->numTokens - 1);
×
2593

2594
    }//end findEndOfStatement()
2595

2596

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

2623
        $foundToken = false;
×
2624

2625
        for ($i = $start; $i >= 0; $i--) {
×
2626
            if ($this->tokens[$i]['line'] < $this->tokens[$start]['line']) {
×
2627
                break;
×
2628
            }
2629

2630
            $found = $exclude;
×
2631
            foreach ($types as $type) {
×
2632
                if ($exclude === false) {
×
2633
                    if ($this->tokens[$i]['code'] === $type) {
×
2634
                        $found = true;
×
2635
                        break;
×
2636
                    }
2637
                } else {
2638
                    if ($this->tokens[$i]['code'] === $type) {
×
2639
                        $found = false;
×
2640
                        break;
×
2641
                    }
2642
                }
2643
            }
2644

2645
            if ($found === true) {
×
2646
                if ($value === null) {
×
2647
                    $foundToken = $i;
×
2648
                } else if ($this->tokens[$i]['content'] === $value) {
×
2649
                    $foundToken = $i;
×
2650
                }
2651
            }
2652
        }//end for
2653

2654
        return $foundToken;
×
2655

2656
    }//end findFirstOnLine()
2657

2658

2659
    /**
2660
     * Determine if the passed token has a condition of one of the passed types.
2661
     *
2662
     * @param int              $stackPtr The position of the token we are checking.
2663
     * @param int|string|array $types    The type(s) of tokens to search for.
2664
     *
2665
     * @return boolean
2666
     */
2667
    public function hasCondition($stackPtr, $types)
21✔
2668
    {
2669
        // Check for the existence of the token.
2670
        if (isset($this->tokens[$stackPtr]) === false) {
21✔
2671
            return false;
3✔
2672
        }
2673

2674
        // Make sure the token has conditions.
2675
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
18✔
2676
            return false;
3✔
2677
        }
2678

2679
        $types      = (array) $types;
15✔
2680
        $conditions = $this->tokens[$stackPtr]['conditions'];
15✔
2681

2682
        foreach ($types as $type) {
15✔
2683
            if (in_array($type, $conditions, true) === true) {
15✔
2684
                // We found a token with the required type.
2685
                return true;
15✔
2686
            }
2687
        }
2688

2689
        return false;
15✔
2690

2691
    }//end hasCondition()
2692

2693

2694
    /**
2695
     * Return the position of the condition for the passed token.
2696
     *
2697
     * Returns FALSE if the token does not have the condition.
2698
     *
2699
     * @param int        $stackPtr The position of the token we are checking.
2700
     * @param int|string $type     The type of token to search for.
2701
     * @param bool       $first    If TRUE, will return the matched condition
2702
     *                             furthest away from the passed token.
2703
     *                             If FALSE, will return the matched condition
2704
     *                             closest to the passed token.
2705
     *
2706
     * @return int|false
2707
     */
2708
    public function getCondition($stackPtr, $type, $first=true)
30✔
2709
    {
2710
        // Check for the existence of the token.
2711
        if (isset($this->tokens[$stackPtr]) === false) {
30✔
2712
            return false;
3✔
2713
        }
2714

2715
        // Make sure the token has conditions.
2716
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
27✔
2717
            return false;
3✔
2718
        }
2719

2720
        $conditions = $this->tokens[$stackPtr]['conditions'];
24✔
2721
        if ($first === false) {
24✔
2722
            $conditions = array_reverse($conditions, true);
12✔
2723
        }
2724

2725
        foreach ($conditions as $token => $condition) {
24✔
2726
            if ($condition === $type) {
24✔
2727
                return $token;
24✔
2728
            }
2729
        }
2730

2731
        return false;
24✔
2732

2733
    }//end getCondition()
2734

2735

2736
    /**
2737
     * Returns the name of the class that the specified class extends.
2738
     * (works for classes, anonymous classes and interfaces)
2739
     *
2740
     * Returns FALSE on error or if there is no extended class name.
2741
     *
2742
     * @param int $stackPtr The stack position of the class.
2743
     *
2744
     * @return string|false
2745
     */
2746
    public function findExtendedClassName($stackPtr)
54✔
2747
    {
2748
        // Check for the existence of the token.
2749
        if (isset($this->tokens[$stackPtr]) === false) {
54✔
2750
            return false;
3✔
2751
        }
2752

2753
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
51✔
2754
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
51✔
2755
            && $this->tokens[$stackPtr]['code'] !== T_INTERFACE
51✔
2756
        ) {
2757
            return false;
3✔
2758
        }
2759

2760
        if (isset($this->tokens[$stackPtr]['scope_opener']) === false) {
48✔
2761
            return false;
3✔
2762
        }
2763

2764
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
45✔
2765
        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex);
45✔
2766
        if ($extendsIndex === false) {
45✔
2767
            return false;
9✔
2768
        }
2769

2770
        $find = [
24✔
2771
            T_STRING,
36✔
2772
            T_NAME_QUALIFIED,
36✔
2773
            T_NAME_FULLY_QUALIFIED,
36✔
2774
            T_NAME_RELATIVE,
36✔
2775
            T_WHITESPACE,
36✔
2776
        ];
24✔
2777

2778
        $end  = $this->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true);
36✔
2779
        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
36✔
2780
        $name = trim($name);
36✔
2781

2782
        if ($name === '') {
36✔
2783
            return false;
3✔
2784
        }
2785

2786
        return $name;
33✔
2787

2788
    }//end findExtendedClassName()
2789

2790

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

2807
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
48✔
2808
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
48✔
2809
            && $this->tokens[$stackPtr]['code'] !== T_ENUM
48✔
2810
        ) {
2811
            return false;
6✔
2812
        }
2813

2814
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
42✔
2815
            return false;
3✔
2816
        }
2817

2818
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
39✔
2819
        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
39✔
2820
        if ($implementsIndex === false) {
39✔
2821
            return false;
6✔
2822
        }
2823

2824
        $find = [
22✔
2825
            T_STRING,
33✔
2826
            T_NAME_QUALIFIED,
33✔
2827
            T_NAME_FULLY_QUALIFIED,
33✔
2828
            T_NAME_RELATIVE,
33✔
2829
            T_WHITESPACE,
33✔
2830
            T_COMMA,
33✔
2831
        ];
22✔
2832

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

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

2845
    }//end findImplementedInterfaceNames()
2846

2847

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

© 2025 Coveralls, Inc