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

PHPCSStandards / PHP_CodeSniffer / 14517893327

17 Apr 2025 02:21PM UTC coverage: 77.984% (-0.006%) from 77.99%
14517893327

push

github

web-flow
Merge pull request #1013 from PHPCSStandards/phpcs-4.0/feature/sq-2593-tokenizer-closure-use-parentheses-owner

Tokenizer: T_USE tokens for closure use now contain parenthesis information

32 of 34 new or added lines in 9 files covered. (94.12%)

1 existing line in 1 file now uncovered.

19439 of 24927 relevant lines covered (77.98%)

79.12 hits per line

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

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

10
namespace PHP_CodeSniffer\Files;
11

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

22
class File
23
{
24

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

221

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

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

248
    }//end __construct()
249

250

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

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

272
    }//end setContent()
273

274

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

286
    }//end reloadContent()
×
287

288

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

298
    }//end disableCaching()
299

300

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

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

318
        $this->parse();
×
319

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

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

327
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
328
            StatusWriter::write('*** START TOKEN PROCESSING ***', 1);
×
329
        }
330

331
        $foundCode        = false;
×
332
        $listenerIgnoreTo = [];
×
333
        $inTests          = defined('PHP_CODESNIFFER_IN_TESTS');
×
334
        $checkAnnotations = $this->config->annotations;
×
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
                            $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $settings);
×
373
                        }
374
                    }
375
                }//end if
376
            }//end if
377

378
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
379
                $type    = $token['type'];
×
380
                $content = Common::prepareForOutput($token['content']);
×
381
                StatusWriter::write("Process token $stackPtr: $type => $content", 2);
×
382
            }
383

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

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

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

401
                $class = $listenerData['class'];
×
402

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

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

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

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

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

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

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

455
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
456
                    StatusWriter::write('Processing '.$this->activeListener.'... ', 3, 0);
×
457
                }
458

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

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

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

473
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
474
                    $timeTaken = round(($timeTaken), 4);
×
475
                    StatusWriter::write("DONE in $timeTaken seconds");
×
476
                }
477

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

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

496
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
×
497
            StatusWriter::write('*** END TOKEN PROCESSING ***', 1);
×
498
            StatusWriter::write('*** START SNIFF PROCESSING REPORT ***', 1);
×
499

500
            arsort($this->listenerTimes, SORT_NUMERIC);
×
501
            foreach ($this->listenerTimes as $listener => $timeTaken) {
×
502
                StatusWriter::write("$listener: ".round(($timeTaken), 4).' secs', 1);
×
503
            }
504

505
            StatusWriter::write('*** END SNIFF PROCESSING REPORT ***', 1);
×
506
        }
507

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

510
    }//end process()
511

512

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

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

537
                StatusWriter::write('[tokenizer error]... ', 0, $newlines);
×
538
            }
539

540
            return;
×
541
        }
542

543
        $this->numTokens = count($this->tokens);
×
544

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

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

566
            $newlines = 0;
×
567
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
568
                $newlines = 1;
×
569
            }
570

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

574
    }//end parse()
575

576

577
    /**
578
     * Returns the token stack for this file.
579
     *
580
     * @return array
581
     */
582
    public function getTokens()
×
583
    {
584
        return $this->tokens;
×
585

586
    }//end getTokens()
587

588

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

605
    }//end cleanUp()
606

607

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

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

639
    }//end addError()
640

641

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

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

673
    }//end addWarning()
674

675

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

697
    }//end addErrorOnLine()
698

699

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

721
    }//end addWarningOnLine()
722

723

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

750
        return false;
×
751

752
    }//end addFixableError()
753

754

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

781
        return false;
×
782

783
    }//end addFixableWarning()
784

785

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

808
        // Work out which sniff generated the message.
809
        $parts = explode('.', $code);
186✔
810
        if ($parts[0] === 'Internal') {
186✔
811
            // An internal message.
812
            $listenerCode = '';
18✔
813
            if ($this->activeListener !== '') {
18✔
814
                $listenerCode = Common::getSniffCode($this->activeListener);
×
815
            }
816

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

830
            $checkCodes = [
112✔
831
                $sniffCode,
168✔
832
                $parts[0].'.'.$parts[1].'.'.$parts[2],
168✔
833
                $parts[0].'.'.$parts[1],
168✔
834
                $parts[0],
168✔
835
            ];
112✔
836
        }//end if
837

838
        if (isset($this->tokenizer->ignoredLines[$line]) === true) {
186✔
839
            // Check if this line is ignoring this specific message.
840
            $ignored = false;
99✔
841
            foreach ($checkCodes as $checkCode) {
99✔
842
                if (isset($this->tokenizer->ignoredLines[$line][$checkCode]) === true) {
99✔
843
                    $ignored = true;
90✔
844
                    break;
90✔
845
                }
846
            }
847

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

860
            if ($ignored === true) {
99✔
861
                return false;
90✔
862
            }
863
        }//end if
864

865
        $includeAll = true;
174✔
866
        if ($this->configCache['cache'] === false
174✔
867
            || $this->configCache['recordErrors'] === false
174✔
868
        ) {
869
            $includeAll = false;
174✔
870
        }
871

872
        // Filter out any messages for sniffs that shouldn't have run
873
        // due to the use of the --sniffs or --exclude command line argument,
874
        // but don't filter out "Internal" messages.
875
        if ($includeAll === false
174✔
876
            && (($parts[0] !== 'Internal'
174✔
877
            && empty($this->configCache['sniffs']) === false
174✔
878
            && in_array(strtolower($listenerCode), $this->configCache['sniffs'], true) === false)
168✔
879
            || (empty($this->configCache['exclude']) === false
174✔
880
            && in_array(strtolower($listenerCode), $this->configCache['exclude'], true) === true))
174✔
881
        ) {
882
            return false;
×
883
        }
884

885
        // If we know this sniff code is being ignored for this file, return early.
886
        foreach ($checkCodes as $checkCode) {
174✔
887
            if (isset($this->ignoredCodes[$checkCode]) === true) {
174✔
888
                return false;
×
889
            }
890
        }
891

892
        $oppositeType = 'warning';
174✔
893
        if ($error === false) {
174✔
894
            $oppositeType = 'error';
75✔
895
        }
896

897
        foreach ($checkCodes as $checkCode) {
174✔
898
            // Make sure this message type has not been set to the opposite message type.
899
            if (isset($this->ruleset->ruleset[$checkCode]['type']) === true
174✔
900
                && $this->ruleset->ruleset[$checkCode]['type'] === $oppositeType
174✔
901
            ) {
902
                $error = !$error;
×
903
                break;
×
904
            }
905
        }
906

907
        if ($error === true) {
174✔
908
            $configSeverity = $this->configCache['errorSeverity'];
150✔
909
            $messageCount   = &$this->errorCount;
150✔
910
            $messages       = &$this->errors;
150✔
911
        } else {
912
            $configSeverity = $this->configCache['warningSeverity'];
75✔
913
            $messageCount   = &$this->warningCount;
75✔
914
            $messages       = &$this->warnings;
75✔
915
        }
916

917
        if ($includeAll === false && $configSeverity === 0) {
174✔
918
            // Don't bother doing any processing as these messages are just going to
919
            // be hidden in the reports anyway.
920
            return false;
×
921
        }
922

923
        if ($severity === 0) {
174✔
924
            $severity = 5;
174✔
925
        }
926

927
        foreach ($checkCodes as $checkCode) {
174✔
928
            // Make sure we are interested in this severity level.
929
            if (isset($this->ruleset->ruleset[$checkCode]['severity']) === true) {
174✔
930
                $severity = $this->ruleset->ruleset[$checkCode]['severity'];
9✔
931
                break;
9✔
932
            }
933
        }
934

935
        if ($includeAll === false && $configSeverity > $severity) {
174✔
936
            return false;
9✔
937
        }
938

939
        // Make sure we are not ignoring this file.
940
        $included = null;
165✔
941
        if (trim($this->path, '\'"') === 'STDIN') {
165✔
942
            $included = true;
165✔
943
        } else {
944
            foreach ($checkCodes as $checkCode) {
×
945
                $patterns = null;
×
946

947
                if (isset($this->configCache['includePatterns'][$checkCode]) === true) {
×
948
                    $patterns  = $this->configCache['includePatterns'][$checkCode];
×
949
                    $excluding = false;
×
950
                } else if (isset($this->configCache['ignorePatterns'][$checkCode]) === true) {
×
951
                    $patterns  = $this->configCache['ignorePatterns'][$checkCode];
×
952
                    $excluding = true;
×
953
                }
954

955
                if ($patterns === null) {
×
956
                    continue;
×
957
                }
958

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

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

974
                    $pattern = '`'.strtr($pattern, $replacements).'`i';
×
975
                    $matched = preg_match($pattern, $this->path);
×
976

977
                    if ($matched === 0) {
×
978
                        if ($excluding === false && $included === null) {
×
979
                            // This file path is not being included.
980
                            $included = false;
×
981
                        }
982

983
                        continue;
×
984
                    }
985

986
                    if ($excluding === true) {
×
987
                        // This file path is being excluded.
988
                        $this->ignoredCodes[$checkCode] = true;
×
989
                        return false;
×
990
                    }
991

992
                    // This file path is being included.
993
                    $included = true;
×
994
                    break;
×
995
                }//end foreach
996
            }//end foreach
997
        }//end if
998

999
        if ($included === false) {
165✔
1000
            // There were include rules set, but this file
1001
            // path didn't match any of them.
1002
            return false;
×
1003
        }
1004

1005
        $messageCount++;
165✔
1006
        if ($fixable === true) {
165✔
1007
            $this->fixableCount++;
132✔
1008
        }
1009

1010
        if ($this->configCache['recordErrors'] === false
165✔
1011
            && $includeAll === false
165✔
1012
        ) {
1013
            return true;
×
1014
        }
1015

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

1026
        if (empty($data) === false) {
165✔
1027
            $message = vsprintf($message, $data);
153✔
1028
        }
1029

1030
        if (isset($messages[$line]) === false) {
165✔
1031
            $messages[$line] = [];
165✔
1032
        }
1033

1034
        if (isset($messages[$line][$column]) === false) {
165✔
1035
            $messages[$line][$column] = [];
165✔
1036
        }
1037

1038
        $messages[$line][$column][] = [
165✔
1039
            'message'  => $message,
165✔
1040
            'source'   => $sniffCode,
165✔
1041
            'listener' => $this->activeListener,
165✔
1042
            'severity' => $severity,
165✔
1043
            'fixable'  => $fixable,
165✔
1044
        ];
110✔
1045

1046
        if (PHP_CODESNIFFER_VERBOSITY > 1
165✔
1047
            && $this->fixer->enabled === true
165✔
1048
            && $fixable === true
165✔
1049
        ) {
1050
            @ob_end_clean();
×
1051
            StatusWriter::forceWrite("E: [Line $line] $message ($sniffCode)", 1);
×
1052
            ob_start();
×
1053
        }
1054

1055
        return true;
165✔
1056

1057
    }//end addMessage()
1058

1059

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

1083
        return true;
×
1084

1085
    }//end recordMetric()
1086

1087

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

1097
    }//end getErrorCount()
1098

1099

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

1109
    }//end getWarningCount()
1110

1111

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

1121
    }//end getFixableCount()
1122

1123

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

1133
    }//end getFixedCount()
1134

1135

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

1145
    }//end getIgnoredLines()
1146

1147

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

1157
    }//end getErrors()
1158

1159

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

1169
    }//end getWarnings()
1170

1171

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

1181
    }//end getMetrics()
1182

1183

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

1193
    }//end getListenerTimes()
1194

1195

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

1205
    }//end getFilename()
1206

1207

1208
    /**
1209
     * Returns the declaration name for classes, interfaces, traits, enums, and functions.
1210
     *
1211
     * @param int $stackPtr The position of the declaration token which
1212
     *                      declared the class, interface, trait, or function.
1213
     *
1214
     * @return string The name of the class, interface, trait, or function or an empty string
1215
     *                if the name could not be determined (live coding).
1216
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type
1217
     *                                                      T_FUNCTION, T_CLASS, T_TRAIT, T_ENUM, or T_INTERFACE.
1218
     */
1219
    public function getDeclarationName($stackPtr)
87✔
1220
    {
1221
        $tokenCode = $this->tokens[$stackPtr]['code'];
87✔
1222

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

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

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

1249
        return $content;
69✔
1250

1251
    }//end getDeclarationName()
1252

1253

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

1316
        if ($this->tokens[$stackPtr]['code'] === T_USE
228✔
1317
            && isset($this->tokens[$stackPtr]['parenthesis_owner']) === false
228✔
1318
        ) {
1319
            throw new RuntimeException('$stackPtr was not a valid T_USE');
12✔
1320
        }
1321

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

1327
        $opener = $this->tokens[$stackPtr]['parenthesis_opener'];
213✔
1328

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1566
        return $vars;
207✔
1567

1568
    }//end getMethodParameters()
1569

1570

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

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

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

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

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

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

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

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

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

1701
                // Skip over closure use statements.
1702
                if ($this->tokens[$i]['code'] === T_USE) {
168✔
1703
                    if (isset($this->tokens[$i]['parenthesis_closer']) === false) {
15✔
1704
                        // Live coding/parse error, stop parsing.
NEW
1705
                        break;
×
1706
                    }
1707

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

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

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

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

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

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

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

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

1753
    }//end getMethodProperties()
1754

1755

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

1792
        $conditions = $this->tokens[$stackPtr]['conditions'];
297✔
1793
        $conditions = array_keys($conditions);
297✔
1794
        $ptr        = array_pop($conditions);
297✔
1795
        if (isset($this->tokens[$ptr]) === false
297✔
1796
            || isset(Tokens::$ooScopeTokens[$this->tokens[$ptr]['code']]) === false
294✔
1797
            || $this->tokens[$ptr]['code'] === T_ENUM
297✔
1798
        ) {
1799
            throw new RuntimeException('$stackPtr is not a class member var');
15✔
1800
        }
1801

1802
        // Make sure it's not a method parameter.
1803
        if (empty($this->tokens[$stackPtr]['nested_parenthesis']) === false) {
282✔
1804
            $parenthesis = array_keys($this->tokens[$stackPtr]['nested_parenthesis']);
15✔
1805
            $deepestOpen = array_pop($parenthesis);
15✔
1806
            if ($deepestOpen > $ptr
15✔
1807
                && isset($this->tokens[$deepestOpen]['parenthesis_owner']) === true
15✔
1808
                && $this->tokens[$this->tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION
15✔
1809
            ) {
1810
                throw new RuntimeException('$stackPtr is not a class member var');
9✔
1811
            }
1812
        }
1813

1814
        $valid = [
182✔
1815
            T_PUBLIC    => T_PUBLIC,
273✔
1816
            T_PRIVATE   => T_PRIVATE,
273✔
1817
            T_PROTECTED => T_PROTECTED,
273✔
1818
            T_STATIC    => T_STATIC,
273✔
1819
            T_VAR       => T_VAR,
273✔
1820
            T_READONLY  => T_READONLY,
273✔
1821
            T_FINAL     => T_FINAL,
273✔
1822
        ];
182✔
1823

1824
        $valid += Tokens::$emptyTokens;
273✔
1825

1826
        $scope          = 'public';
273✔
1827
        $scopeSpecified = false;
273✔
1828
        $isStatic       = false;
273✔
1829
        $isReadonly     = false;
273✔
1830
        $isFinal        = false;
273✔
1831

1832
        $startOfStatement = $this->findPrevious(
273✔
1833
            [
182✔
1834
                T_SEMICOLON,
273✔
1835
                T_OPEN_CURLY_BRACKET,
273✔
1836
                T_CLOSE_CURLY_BRACKET,
273✔
1837
                T_ATTRIBUTE_END,
273✔
1838
            ],
182✔
1839
            ($stackPtr - 1)
273✔
1840
        );
182✔
1841

1842
        for ($i = ($startOfStatement + 1); $i < $stackPtr; $i++) {
273✔
1843
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
273✔
1844
                break;
207✔
1845
            }
1846

1847
            switch ($this->tokens[$i]['code']) {
273✔
1848
            case T_PUBLIC:
273✔
1849
                $scope          = 'public';
129✔
1850
                $scopeSpecified = true;
129✔
1851
                break;
129✔
1852
            case T_PRIVATE:
273✔
1853
                $scope          = 'private';
57✔
1854
                $scopeSpecified = true;
57✔
1855
                break;
57✔
1856
            case T_PROTECTED:
273✔
1857
                $scope          = 'protected';
45✔
1858
                $scopeSpecified = true;
45✔
1859
                break;
45✔
1860
            case T_STATIC:
273✔
1861
                $isStatic = true;
66✔
1862
                break;
66✔
1863
            case T_READONLY:
273✔
1864
                $isReadonly = true;
36✔
1865
                break;
36✔
1866
            case T_FINAL:
273✔
1867
                $isFinal = true;
27✔
1868
                break;
27✔
1869
            }//end switch
1870
        }//end for
1871

1872
        $type         = '';
273✔
1873
        $typeToken    = false;
273✔
1874
        $typeEndToken = false;
273✔
1875
        $nullableType = false;
273✔
1876

1877
        if ($i < $stackPtr) {
273✔
1878
            // We've found a type.
1879
            $valid = [
138✔
1880
                T_STRING                 => T_STRING,
207✔
1881
                T_CALLABLE               => T_CALLABLE,
207✔
1882
                T_SELF                   => T_SELF,
207✔
1883
                T_PARENT                 => T_PARENT,
207✔
1884
                T_FALSE                  => T_FALSE,
207✔
1885
                T_TRUE                   => T_TRUE,
207✔
1886
                T_NULL                   => T_NULL,
207✔
1887
                T_NAMESPACE              => T_NAMESPACE,
207✔
1888
                T_NS_SEPARATOR           => T_NS_SEPARATOR,
207✔
1889
                T_TYPE_UNION             => T_TYPE_UNION,
207✔
1890
                T_TYPE_INTERSECTION      => T_TYPE_INTERSECTION,
207✔
1891
                T_TYPE_OPEN_PARENTHESIS  => T_TYPE_OPEN_PARENTHESIS,
207✔
1892
                T_TYPE_CLOSE_PARENTHESIS => T_TYPE_CLOSE_PARENTHESIS,
207✔
1893
            ];
138✔
1894

1895
            for ($i; $i < $stackPtr; $i++) {
207✔
1896
                if ($this->tokens[$i]['code'] === T_VARIABLE) {
207✔
1897
                    // Hit another variable in a group definition.
1898
                    break;
30✔
1899
                }
1900

1901
                if ($this->tokens[$i]['code'] === T_NULLABLE) {
183✔
1902
                    $nullableType = true;
51✔
1903
                }
1904

1905
                if (isset($valid[$this->tokens[$i]['code']]) === true) {
183✔
1906
                    $typeEndToken = $i;
183✔
1907
                    if ($typeToken === false) {
183✔
1908
                        $typeToken = $i;
183✔
1909
                    }
1910

1911
                    $type .= $this->tokens[$i]['content'];
183✔
1912
                }
1913
            }
1914

1915
            if ($type !== '' && $nullableType === true) {
207✔
1916
                $type = '?'.$type;
51✔
1917
            }
1918
        }//end if
1919

1920
        return [
182✔
1921
            'scope'           => $scope,
273✔
1922
            'scope_specified' => $scopeSpecified,
273✔
1923
            'is_static'       => $isStatic,
273✔
1924
            'is_readonly'     => $isReadonly,
273✔
1925
            'is_final'        => $isFinal,
273✔
1926
            'type'            => $type,
273✔
1927
            'type_token'      => $typeToken,
273✔
1928
            'type_end_token'  => $typeEndToken,
273✔
1929
            'nullable_type'   => $nullableType,
273✔
1930
        ];
182✔
1931

1932
    }//end getMemberProperties()
1933

1934

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

1960
        $valid = [
22✔
1961
            T_FINAL      => T_FINAL,
33✔
1962
            T_ABSTRACT   => T_ABSTRACT,
33✔
1963
            T_READONLY   => T_READONLY,
33✔
1964
            T_WHITESPACE => T_WHITESPACE,
33✔
1965
            T_COMMENT    => T_COMMENT,
33✔
1966
        ];
22✔
1967

1968
        $isAbstract = false;
33✔
1969
        $isFinal    = false;
33✔
1970
        $isReadonly = false;
33✔
1971

1972
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
33✔
1973
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
33✔
1974
                break;
33✔
1975
            }
1976

1977
            switch ($this->tokens[$i]['code']) {
33✔
1978
            case T_ABSTRACT:
33✔
1979
                $isAbstract = true;
15✔
1980
                break;
15✔
1981

1982
            case T_FINAL:
33✔
1983
                $isFinal = true;
12✔
1984
                break;
12✔
1985

1986
            case T_READONLY:
33✔
1987
                $isReadonly = true;
15✔
1988
                break;
15✔
1989
            }
1990
        }//end for
1991

1992
        return [
22✔
1993
            'is_abstract' => $isAbstract,
33✔
1994
            'is_final'    => $isFinal,
33✔
1995
            'is_readonly' => $isReadonly,
33✔
1996
        ];
22✔
1997

1998
    }//end getClassProperties()
1999

2000

2001
    /**
2002
     * Determine if the passed token is a reference operator.
2003
     *
2004
     * Returns true if the specified token position represents a reference.
2005
     * Returns false if the token represents a bitwise operator.
2006
     *
2007
     * @param int $stackPtr The position of the T_BITWISE_AND token.
2008
     *
2009
     * @return boolean
2010
     */
2011
    public function isReference($stackPtr)
228✔
2012
    {
2013
        if ($this->tokens[$stackPtr]['code'] !== T_BITWISE_AND) {
228✔
2014
            return false;
9✔
2015
        }
2016

2017
        $tokenBefore = $this->findPrevious(
219✔
2018
            Tokens::$emptyTokens,
219✔
2019
            ($stackPtr - 1),
219✔
2020
            null,
219✔
2021
            true
219✔
2022
        );
146✔
2023

2024
        if ($this->tokens[$tokenBefore]['code'] === T_FUNCTION
219✔
2025
            || $this->tokens[$tokenBefore]['code'] === T_CLOSURE
216✔
2026
            || $this->tokens[$tokenBefore]['code'] === T_FN
219✔
2027
        ) {
2028
            // Function returns a reference.
2029
            return true;
9✔
2030
        }
2031

2032
        if ($this->tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
210✔
2033
            // Inside a foreach loop or array assignment, this is a reference.
2034
            return true;
18✔
2035
        }
2036

2037
        if ($this->tokens[$tokenBefore]['code'] === T_AS) {
192✔
2038
            // Inside a foreach loop, this is a reference.
2039
            return true;
3✔
2040
        }
2041

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

2048
        $tokenAfter = $this->findNext(
168✔
2049
            Tokens::$emptyTokens,
168✔
2050
            ($stackPtr + 1),
168✔
2051
            null,
168✔
2052
            true
168✔
2053
        );
112✔
2054

2055
        if ($this->tokens[$tokenAfter]['code'] === T_NEW) {
168✔
2056
            return true;
3✔
2057
        }
2058

2059
        if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === true) {
165✔
2060
            $brackets    = $this->tokens[$stackPtr]['nested_parenthesis'];
117✔
2061
            $lastBracket = array_pop($brackets);
117✔
2062
            if (isset($this->tokens[$lastBracket]['parenthesis_owner']) === true) {
117✔
2063
                $owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']];
81✔
2064
                if ($owner['code'] === T_FUNCTION
81✔
2065
                    || $owner['code'] === T_CLOSURE
69✔
2066
                    || $owner['code'] === T_FN
42✔
2067
                    || $owner['code'] === T_USE
81✔
2068
                ) {
2069
                    $params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']);
60✔
2070
                    foreach ($params as $param) {
60✔
2071
                        if ($param['reference_token'] === $stackPtr) {
60✔
2072
                            // Function parameter declared to be passed by reference.
2073
                            return true;
45✔
2074
                        }
2075
                    }
2076
                }//end if
2077
            }//end if
2078
        }//end if
2079

2080
        // Pass by reference in function calls and assign by reference in arrays.
2081
        if ($this->tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
120✔
2082
            || $this->tokens[$tokenBefore]['code'] === T_COMMA
108✔
2083
            || $this->tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY
120✔
2084
        ) {
2085
            if ($this->tokens[$tokenAfter]['code'] === T_VARIABLE) {
84✔
2086
                return true;
60✔
2087
            } else {
2088
                $skip   = Tokens::$emptyTokens;
24✔
2089
                $skip[] = T_NS_SEPARATOR;
24✔
2090
                $skip[] = T_SELF;
24✔
2091
                $skip[] = T_PARENT;
24✔
2092
                $skip[] = T_STATIC;
24✔
2093
                $skip[] = T_STRING;
24✔
2094
                $skip[] = T_NAMESPACE;
24✔
2095
                $skip[] = T_DOUBLE_COLON;
24✔
2096

2097
                $nextSignificantAfter = $this->findNext(
24✔
2098
                    $skip,
24✔
2099
                    ($stackPtr + 1),
24✔
2100
                    null,
24✔
2101
                    true
24✔
2102
                );
16✔
2103
                if ($this->tokens[$nextSignificantAfter]['code'] === T_VARIABLE) {
24✔
2104
                    return true;
24✔
2105
                }
2106
            }//end if
2107
        }//end if
2108

2109
        return false;
36✔
2110

2111
    }//end isReference()
2112

2113

2114
    /**
2115
     * Returns the content of the tokens from the specified start position in
2116
     * the token stack for the specified length.
2117
     *
2118
     * @param int  $start       The position to start from in the token stack.
2119
     * @param int  $length      The length of tokens to traverse from the start pos.
2120
     * @param bool $origContent Whether the original content or the tab replaced
2121
     *                          content should be used.
2122
     *
2123
     * @return string The token contents.
2124
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position does not exist.
2125
     */
2126
    public function getTokensAsString($start, $length, $origContent=false)
84✔
2127
    {
2128
        if (is_int($start) === false || isset($this->tokens[$start]) === false) {
84✔
2129
            throw new RuntimeException('The $start position for getTokensAsString() must exist in the token stack');
6✔
2130
        }
2131

2132
        if (is_int($length) === false || $length <= 0) {
78✔
2133
            return '';
9✔
2134
        }
2135

2136
        $str = '';
69✔
2137
        $end = ($start + $length);
69✔
2138
        if ($end > $this->numTokens) {
69✔
2139
            $end = $this->numTokens;
3✔
2140
        }
2141

2142
        for ($i = $start; $i < $end; $i++) {
69✔
2143
            // If tabs are being converted to spaces by the tokeniser, the
2144
            // original content should be used instead of the converted content.
2145
            if ($origContent === true && isset($this->tokens[$i]['orig_content']) === true) {
69✔
2146
                $str .= $this->tokens[$i]['orig_content'];
6✔
2147
            } else {
2148
                $str .= $this->tokens[$i]['content'];
69✔
2149
            }
2150
        }
2151

2152
        return $str;
69✔
2153

2154
    }//end getTokensAsString()
2155

2156

2157
    /**
2158
     * Returns the position of the previous specified token(s).
2159
     *
2160
     * If a value is specified, the previous token of the specified type(s)
2161
     * containing the specified value will be returned.
2162
     *
2163
     * Returns false if no token can be found.
2164
     *
2165
     * @param int|string|array $types   The type(s) of tokens to search for.
2166
     * @param int              $start   The position to start searching from in the
2167
     *                                  token stack.
2168
     * @param int|null         $end     The end position to fail if no token is found.
2169
     *                                  if not specified or null, end will default to
2170
     *                                  the start of the token stack.
2171
     * @param bool             $exclude If true, find the previous token that is NOT of
2172
     *                                  the types specified in $types.
2173
     * @param string|null      $value   The value that the token(s) must be equal to.
2174
     *                                  If value is omitted, tokens with any value will
2175
     *                                  be returned.
2176
     * @param bool             $local   If true, tokens outside the current statement
2177
     *                                  will not be checked. IE. checking will stop
2178
     *                                  at the previous semicolon found.
2179
     *
2180
     * @return int|false
2181
     * @see    findNext()
2182
     */
2183
    public function findPrevious(
×
2184
        $types,
2185
        $start,
2186
        $end=null,
2187
        $exclude=false,
2188
        $value=null,
2189
        $local=false
2190
    ) {
2191
        $types = (array) $types;
×
2192

2193
        if ($end === null) {
×
2194
            $end = 0;
×
2195
        }
2196

2197
        for ($i = $start; $i >= $end; $i--) {
×
2198
            $found = (bool) $exclude;
×
2199
            foreach ($types as $type) {
×
2200
                if ($this->tokens[$i]['code'] === $type) {
×
2201
                    $found = !$exclude;
×
2202
                    break;
×
2203
                }
2204
            }
2205

2206
            if ($found === true) {
×
2207
                if ($value === null) {
×
2208
                    return $i;
×
2209
                } else if ($this->tokens[$i]['content'] === $value) {
×
2210
                    return $i;
×
2211
                }
2212
            }
2213

2214
            if ($local === true) {
×
2215
                if (isset($this->tokens[$i]['scope_opener']) === true
×
2216
                    && $i === $this->tokens[$i]['scope_closer']
×
2217
                ) {
2218
                    $i = $this->tokens[$i]['scope_opener'];
×
2219
                } else if (isset($this->tokens[$i]['bracket_opener']) === true
×
2220
                    && $i === $this->tokens[$i]['bracket_closer']
×
2221
                ) {
2222
                    $i = $this->tokens[$i]['bracket_opener'];
×
2223
                } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
×
2224
                    && $i === $this->tokens[$i]['parenthesis_closer']
×
2225
                ) {
2226
                    $i = $this->tokens[$i]['parenthesis_opener'];
×
2227
                } else if ($this->tokens[$i]['code'] === T_SEMICOLON) {
×
2228
                    break;
×
2229
                }
2230
            }
2231
        }//end for
2232

2233
        return false;
×
2234

2235
    }//end findPrevious()
2236

2237

2238
    /**
2239
     * Returns the position of the next specified token(s).
2240
     *
2241
     * If a value is specified, the next token of the specified type(s)
2242
     * containing the specified value will be returned.
2243
     *
2244
     * Returns false if no token can be found.
2245
     *
2246
     * @param int|string|array $types   The type(s) of tokens to search for.
2247
     * @param int              $start   The position to start searching from in the
2248
     *                                  token stack.
2249
     * @param int|null         $end     The end position to fail if no token is found.
2250
     *                                  if not specified or null, end will default to
2251
     *                                  the end of the token stack.
2252
     * @param bool             $exclude If true, find the next token that is NOT of
2253
     *                                  a type specified in $types.
2254
     * @param string|null      $value   The value that the token(s) must be equal to.
2255
     *                                  If value is omitted, tokens with any value will
2256
     *                                  be returned.
2257
     * @param bool             $local   If true, tokens outside the current statement
2258
     *                                  will not be checked. i.e., checking will stop
2259
     *                                  at the next semicolon found.
2260
     *
2261
     * @return int|false
2262
     * @see    findPrevious()
2263
     */
2264
    public function findNext(
×
2265
        $types,
2266
        $start,
2267
        $end=null,
2268
        $exclude=false,
2269
        $value=null,
2270
        $local=false
2271
    ) {
2272
        $types = (array) $types;
×
2273

2274
        if ($end === null || $end > $this->numTokens) {
×
2275
            $end = $this->numTokens;
×
2276
        }
2277

2278
        for ($i = $start; $i < $end; $i++) {
×
2279
            $found = (bool) $exclude;
×
2280
            foreach ($types as $type) {
×
2281
                if ($this->tokens[$i]['code'] === $type) {
×
2282
                    $found = !$exclude;
×
2283
                    break;
×
2284
                }
2285
            }
2286

2287
            if ($found === true) {
×
2288
                if ($value === null) {
×
2289
                    return $i;
×
2290
                } else if ($this->tokens[$i]['content'] === $value) {
×
2291
                    return $i;
×
2292
                }
2293
            }
2294

2295
            if ($local === true && $this->tokens[$i]['code'] === T_SEMICOLON) {
×
2296
                break;
×
2297
            }
2298
        }//end for
2299

2300
        return false;
×
2301

2302
    }//end findNext()
2303

2304

2305
    /**
2306
     * Returns the position of the first non-whitespace token in a statement.
2307
     *
2308
     * @param int              $start  The position to start searching from in the token stack.
2309
     * @param int|string|array $ignore Token types that should not be considered stop points.
2310
     *
2311
     * @return int
2312
     */
2313
    public function findStartOfStatement($start, $ignore=null)
210✔
2314
    {
2315
        $startTokens = Tokens::$blockOpeners;
210✔
2316
        $startTokens[T_OPEN_SHORT_ARRAY]   = true;
210✔
2317
        $startTokens[T_OPEN_TAG]           = true;
210✔
2318
        $startTokens[T_OPEN_TAG_WITH_ECHO] = true;
210✔
2319

2320
        $endTokens = [
140✔
2321
            T_CLOSE_TAG    => true,
210✔
2322
            T_COLON        => true,
210✔
2323
            T_COMMA        => true,
210✔
2324
            T_DOUBLE_ARROW => true,
210✔
2325
            T_MATCH_ARROW  => true,
210✔
2326
            T_SEMICOLON    => true,
210✔
2327
        ];
140✔
2328

2329
        if ($ignore !== null) {
210✔
2330
            $ignore = (array) $ignore;
×
2331
            foreach ($ignore as $code) {
×
2332
                if (isset($startTokens[$code]) === true) {
×
2333
                    unset($startTokens[$code]);
×
2334
                }
2335

2336
                if (isset($endTokens[$code]) === true) {
×
2337
                    unset($endTokens[$code]);
×
2338
                }
2339
            }
2340
        }
2341

2342
        // If the start token is inside the case part of a match expression,
2343
        // find the start of the condition. If it's in the statement part, find
2344
        // the token that comes after the match arrow.
2345
        if (empty($this->tokens[$start]['conditions']) === false) {
210✔
2346
            $conditions         = $this->tokens[$start]['conditions'];
162✔
2347
            $lastConditionOwner = end($conditions);
162✔
2348
            $matchExpression    = key($conditions);
162✔
2349

2350
            if ($lastConditionOwner === T_MATCH
162✔
2351
                // Check if the $start token is at the same parentheses nesting level as the match token.
2352
                && ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === true
139✔
2353
                && empty($this->tokens[$start]['nested_parenthesis']) === true)
131✔
2354
                || ((empty($this->tokens[$matchExpression]['nested_parenthesis']) === false
125✔
2355
                && empty($this->tokens[$start]['nested_parenthesis']) === false)
125✔
2356
                && $this->tokens[$matchExpression]['nested_parenthesis'] === $this->tokens[$start]['nested_parenthesis']))
162✔
2357
            ) {
2358
                // Walk back to the previous match arrow (if it exists).
2359
                $lastComma          = null;
45✔
2360
                $inNestedExpression = false;
45✔
2361
                for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) {
45✔
2362
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_MATCH_ARROW) {
45✔
2363
                        break;
33✔
2364
                    }
2365

2366
                    if ($prevMatch !== $start && $this->tokens[$prevMatch]['code'] === T_COMMA) {
45✔
2367
                        $lastComma = $prevMatch;
24✔
2368
                        continue;
24✔
2369
                    }
2370

2371
                    // Skip nested statements.
2372
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
45✔
2373
                        && $prevMatch === $this->tokens[$prevMatch]['bracket_closer']
45✔
2374
                    ) {
2375
                        $prevMatch = $this->tokens[$prevMatch]['bracket_opener'];
12✔
2376
                        continue;
12✔
2377
                    }
2378

2379
                    if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true
45✔
2380
                        && $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer']
45✔
2381
                    ) {
2382
                        $prevMatch = $this->tokens[$prevMatch]['parenthesis_opener'];
15✔
2383
                        continue;
15✔
2384
                    }
2385

2386
                    // Stop if we're _within_ a nested short array statement, which may contain comma's too.
2387
                    // No need to deal with parentheses, those are handled above via the `nested_parenthesis` checks.
2388
                    if (isset($this->tokens[$prevMatch]['bracket_opener']) === true
45✔
2389
                        && $this->tokens[$prevMatch]['bracket_closer'] > $start
45✔
2390
                    ) {
2391
                        $inNestedExpression = true;
15✔
2392
                        break;
15✔
2393
                    }
2394
                }//end for
2395

2396
                if ($inNestedExpression === false) {
45✔
2397
                    // $prevMatch will now either be the scope opener or a match arrow.
2398
                    // If it is the scope opener, go the first non-empty token after. $start will have been part of the first condition.
2399
                    if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) {
33✔
2400
                        // We're before the arrow in the first case.
2401
                        $next = $this->findNext(Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true);
12✔
2402
                        if ($next === false) {
12✔
2403
                            // Shouldn't be possible.
2404
                            return $start;
×
2405
                        }
2406

2407
                        return $next;
12✔
2408
                    }
2409

2410
                    // Okay, so we found a match arrow.
2411
                    // If $start was part of the "next" condition, the last comma will be set.
2412
                    // Otherwise, $start must have been part of a return expression.
2413
                    if (isset($lastComma) === true && $lastComma > $prevMatch) {
33✔
2414
                        $prevMatch = $lastComma;
15✔
2415
                    }
2416

2417
                    // In both cases, go to the first non-empty token after.
2418
                    $next = $this->findNext(Tokens::$emptyTokens, ($prevMatch + 1), null, true);
33✔
2419
                    if ($next === false) {
33✔
2420
                        // Shouldn't be possible.
2421
                        return $start;
×
2422
                    }
2423

2424
                    return $next;
33✔
2425
                }//end if
2426
            }//end if
2427
        }//end if
2428

2429
        $lastNotEmpty = $start;
180✔
2430

2431
        // If we are starting at a token that ends a scope block, skip to
2432
        // the start and continue from there.
2433
        // If we are starting at a token that ends a statement, skip this
2434
        // token so we find the true start of the statement.
2435
        while (isset($endTokens[$this->tokens[$start]['code']]) === true
180✔
2436
            || (isset($this->tokens[$start]['scope_condition']) === true
180✔
2437
            && $start === $this->tokens[$start]['scope_closer'])
180✔
2438
        ) {
2439
            if (isset($this->tokens[$start]['scope_condition']) === true) {
51✔
2440
                $start = $this->tokens[$start]['scope_condition'];
27✔
2441
            } else {
2442
                $start--;
30✔
2443
            }
2444
        }
2445

2446
        for ($i = $start; $i >= 0; $i--) {
180✔
2447
            if (isset($startTokens[$this->tokens[$i]['code']]) === true
180✔
2448
                || isset($endTokens[$this->tokens[$i]['code']]) === true
180✔
2449
            ) {
2450
                // Found the end of the previous statement.
2451
                return $lastNotEmpty;
180✔
2452
            }
2453

2454
            if (isset($this->tokens[$i]['scope_opener']) === true
177✔
2455
                && $i === $this->tokens[$i]['scope_closer']
177✔
2456
                && $this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS
177✔
2457
                && $this->tokens[$i]['code'] !== T_END_NOWDOC
177✔
2458
                && $this->tokens[$i]['code'] !== T_END_HEREDOC
177✔
2459
                && $this->tokens[$i]['code'] !== T_BREAK
177✔
2460
                && $this->tokens[$i]['code'] !== T_RETURN
177✔
2461
                && $this->tokens[$i]['code'] !== T_CONTINUE
177✔
2462
                && $this->tokens[$i]['code'] !== T_THROW
177✔
2463
                && $this->tokens[$i]['code'] !== T_EXIT
177✔
2464
                && $this->tokens[$i]['code'] !== T_GOTO
177✔
2465
            ) {
2466
                // Found the end of the previous scope block.
2467
                return $lastNotEmpty;
3✔
2468
            }
2469

2470
            // Skip nested statements.
2471
            if (isset($this->tokens[$i]['bracket_opener']) === true
177✔
2472
                && $i === $this->tokens[$i]['bracket_closer']
177✔
2473
            ) {
2474
                $i = $this->tokens[$i]['bracket_opener'];
3✔
2475
            } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
177✔
2476
                && $i === $this->tokens[$i]['parenthesis_closer']
177✔
2477
            ) {
2478
                $i = $this->tokens[$i]['parenthesis_opener'];
21✔
2479
            } else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) {
177✔
2480
                $start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1));
6✔
2481
                if ($start !== false) {
6✔
2482
                    $i = $start;
6✔
2483
                }
2484
            }//end if
2485

2486
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
177✔
2487
                $lastNotEmpty = $i;
177✔
2488
            }
2489
        }//end for
2490

2491
        return 0;
×
2492

2493
    }//end findStartOfStatement()
2494

2495

2496
    /**
2497
     * Returns the position of the last non-whitespace token in a statement.
2498
     *
2499
     * @param int              $start  The position to start searching from in the token stack.
2500
     * @param int|string|array $ignore Token types that should not be considered stop points.
2501
     *
2502
     * @return int
2503
     */
2504
    public function findEndOfStatement($start, $ignore=null)
66✔
2505
    {
2506
        $endTokens = [
44✔
2507
            T_COLON                => true,
66✔
2508
            T_COMMA                => true,
66✔
2509
            T_DOUBLE_ARROW         => true,
66✔
2510
            T_SEMICOLON            => true,
66✔
2511
            T_CLOSE_PARENTHESIS    => true,
66✔
2512
            T_CLOSE_SQUARE_BRACKET => true,
66✔
2513
            T_CLOSE_CURLY_BRACKET  => true,
66✔
2514
            T_CLOSE_SHORT_ARRAY    => true,
66✔
2515
            T_OPEN_TAG             => true,
66✔
2516
            T_CLOSE_TAG            => true,
66✔
2517
        ];
44✔
2518

2519
        if ($ignore !== null) {
66✔
2520
            $ignore = (array) $ignore;
×
2521
            foreach ($ignore as $code) {
×
2522
                unset($endTokens[$code]);
×
2523
            }
2524
        }
2525

2526
        // If the start token is inside the case part of a match expression,
2527
        // advance to the match arrow and continue looking for the
2528
        // end of the statement from there so that we skip over commas.
2529
        if ($this->tokens[$start]['code'] !== T_MATCH_ARROW) {
66✔
2530
            $matchExpression = $this->getCondition($start, T_MATCH);
66✔
2531
            if ($matchExpression !== false) {
66✔
2532
                $beforeArrow    = true;
30✔
2533
                $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']);
30✔
2534
                if ($prevMatchArrow !== false) {
30✔
2535
                    $prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start);
27✔
2536
                    if ($prevComma === false) {
27✔
2537
                        // No comma between this token and the last match arrow,
2538
                        // so this token exists after the arrow and we can continue
2539
                        // checking as normal.
2540
                        $beforeArrow = false;
12✔
2541
                    }
2542
                }
2543

2544
                if ($beforeArrow === true) {
30✔
2545
                    $nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']);
30✔
2546
                    if ($nextMatchArrow !== false) {
30✔
2547
                        $start = $nextMatchArrow;
30✔
2548
                    }
2549
                }
2550
            }//end if
2551
        }//end if
2552

2553
        $lastNotEmpty = $start;
66✔
2554
        for ($i = $start; $i < $this->numTokens; $i++) {
66✔
2555
            if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
66✔
2556
                // Found the end of the statement.
2557
                if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS
60✔
2558
                    || $this->tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
57✔
2559
                    || $this->tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
57✔
2560
                    || $this->tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
51✔
2561
                    || $this->tokens[$i]['code'] === T_OPEN_TAG
48✔
2562
                    || $this->tokens[$i]['code'] === T_CLOSE_TAG
60✔
2563
                ) {
2564
                    return $lastNotEmpty;
24✔
2565
                }
2566

2567
                return $i;
48✔
2568
            }
2569

2570
            // Skip nested statements.
2571
            if (isset($this->tokens[$i]['scope_closer']) === true
66✔
2572
                && ($i === $this->tokens[$i]['scope_opener']
56✔
2573
                || $i === $this->tokens[$i]['scope_condition'])
66✔
2574
            ) {
2575
                if ($this->tokens[$i]['code'] === T_FN) {
36✔
2576
                    $lastNotEmpty = $this->tokens[$i]['scope_closer'];
18✔
2577
                    $i            = ($this->tokens[$i]['scope_closer'] - 1);
18✔
2578
                    continue;
18✔
2579
                }
2580

2581
                if ($i === $start && isset(Tokens::$scopeOpeners[$this->tokens[$i]['code']]) === true) {
21✔
2582
                    return $this->tokens[$i]['scope_closer'];
9✔
2583
                }
2584

2585
                $i = $this->tokens[$i]['scope_closer'];
15✔
2586
            } else if (isset($this->tokens[$i]['bracket_closer']) === true
48✔
2587
                && $i === $this->tokens[$i]['bracket_opener']
48✔
2588
            ) {
2589
                $i = $this->tokens[$i]['bracket_closer'];
6✔
2590
            } else if (isset($this->tokens[$i]['parenthesis_closer']) === true
48✔
2591
                && $i === $this->tokens[$i]['parenthesis_opener']
48✔
2592
            ) {
2593
                $i = $this->tokens[$i]['parenthesis_closer'];
9✔
2594
            } else if ($this->tokens[$i]['code'] === T_OPEN_USE_GROUP) {
48✔
2595
                $end = $this->findNext(T_CLOSE_USE_GROUP, ($i + 1));
6✔
2596
                if ($end !== false) {
6✔
2597
                    $i = $end;
6✔
2598
                }
2599
            }//end if
2600

2601
            if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
48✔
2602
                $lastNotEmpty = $i;
48✔
2603
            }
2604
        }//end for
2605

2606
        return ($this->numTokens - 1);
3✔
2607

2608
    }//end findEndOfStatement()
2609

2610

2611
    /**
2612
     * Returns the position of the first token on a line, matching given type.
2613
     *
2614
     * Returns false if no token can be found.
2615
     *
2616
     * @param int|string|array $types   The type(s) of tokens to search for.
2617
     * @param int              $start   The position to start searching from in the
2618
     *                                  token stack.
2619
     * @param bool             $exclude If true, find the token that is NOT of
2620
     *                                  the types specified in $types.
2621
     * @param string           $value   The value that the token must be equal to.
2622
     *                                  If value is omitted, tokens with any value will
2623
     *                                  be returned.
2624
     *
2625
     * @return int|false The first token which matches on the line containing the start
2626
     *                   token, between the start of the line and the start token.
2627
     *                   Note: The first token matching might be the start token.
2628
     *                   FALSE when no matching token could be found between the start of
2629
     *                   the line and the start token.
2630
     */
2631
    public function findFirstOnLine($types, $start, $exclude=false, $value=null)
×
2632
    {
2633
        if (is_array($types) === false) {
×
2634
            $types = [$types];
×
2635
        }
2636

2637
        $foundToken = false;
×
2638

2639
        for ($i = $start; $i >= 0; $i--) {
×
2640
            if ($this->tokens[$i]['line'] < $this->tokens[$start]['line']) {
×
2641
                break;
×
2642
            }
2643

2644
            $found = $exclude;
×
2645
            foreach ($types as $type) {
×
2646
                if ($exclude === false) {
×
2647
                    if ($this->tokens[$i]['code'] === $type) {
×
2648
                        $found = true;
×
2649
                        break;
×
2650
                    }
2651
                } else {
2652
                    if ($this->tokens[$i]['code'] === $type) {
×
2653
                        $found = false;
×
2654
                        break;
×
2655
                    }
2656
                }
2657
            }
2658

2659
            if ($found === true) {
×
2660
                if ($value === null) {
×
2661
                    $foundToken = $i;
×
2662
                } else if ($this->tokens[$i]['content'] === $value) {
×
2663
                    $foundToken = $i;
×
2664
                }
2665
            }
2666
        }//end for
2667

2668
        return $foundToken;
×
2669

2670
    }//end findFirstOnLine()
2671

2672

2673
    /**
2674
     * Determine if the passed token has a condition of one of the passed types.
2675
     *
2676
     * @param int              $stackPtr The position of the token we are checking.
2677
     * @param int|string|array $types    The type(s) of tokens to search for.
2678
     *
2679
     * @return boolean
2680
     */
2681
    public function hasCondition($stackPtr, $types)
21✔
2682
    {
2683
        // Check for the existence of the token.
2684
        if (isset($this->tokens[$stackPtr]) === false) {
21✔
2685
            return false;
3✔
2686
        }
2687

2688
        // Make sure the token has conditions.
2689
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
18✔
2690
            return false;
3✔
2691
        }
2692

2693
        $types      = (array) $types;
15✔
2694
        $conditions = $this->tokens[$stackPtr]['conditions'];
15✔
2695

2696
        foreach ($types as $type) {
15✔
2697
            if (in_array($type, $conditions, true) === true) {
15✔
2698
                // We found a token with the required type.
2699
                return true;
15✔
2700
            }
2701
        }
2702

2703
        return false;
15✔
2704

2705
    }//end hasCondition()
2706

2707

2708
    /**
2709
     * Return the position of the condition for the passed token.
2710
     *
2711
     * Returns FALSE if the token does not have the condition.
2712
     *
2713
     * @param int        $stackPtr The position of the token we are checking.
2714
     * @param int|string $type     The type of token to search for.
2715
     * @param bool       $first    If TRUE, will return the matched condition
2716
     *                             furthest away from the passed token.
2717
     *                             If FALSE, will return the matched condition
2718
     *                             closest to the passed token.
2719
     *
2720
     * @return int|false
2721
     */
2722
    public function getCondition($stackPtr, $type, $first=true)
30✔
2723
    {
2724
        // Check for the existence of the token.
2725
        if (isset($this->tokens[$stackPtr]) === false) {
30✔
2726
            return false;
3✔
2727
        }
2728

2729
        // Make sure the token has conditions.
2730
        if (empty($this->tokens[$stackPtr]['conditions']) === true) {
27✔
2731
            return false;
3✔
2732
        }
2733

2734
        $conditions = $this->tokens[$stackPtr]['conditions'];
24✔
2735
        if ($first === false) {
24✔
2736
            $conditions = array_reverse($conditions, true);
12✔
2737
        }
2738

2739
        foreach ($conditions as $token => $condition) {
24✔
2740
            if ($condition === $type) {
24✔
2741
                return $token;
24✔
2742
            }
2743
        }
2744

2745
        return false;
24✔
2746

2747
    }//end getCondition()
2748

2749

2750
    /**
2751
     * Returns the name of the class that the specified class extends.
2752
     * (works for classes, anonymous classes and interfaces)
2753
     *
2754
     * Returns FALSE on error or if there is no extended class name.
2755
     *
2756
     * @param int $stackPtr The stack position of the class.
2757
     *
2758
     * @return string|false
2759
     */
2760
    public function findExtendedClassName($stackPtr)
51✔
2761
    {
2762
        // Check for the existence of the token.
2763
        if (isset($this->tokens[$stackPtr]) === false) {
51✔
2764
            return false;
3✔
2765
        }
2766

2767
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
48✔
2768
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
48✔
2769
            && $this->tokens[$stackPtr]['code'] !== T_INTERFACE
48✔
2770
        ) {
2771
            return false;
3✔
2772
        }
2773

2774
        if (isset($this->tokens[$stackPtr]['scope_opener']) === false) {
45✔
2775
            return false;
3✔
2776
        }
2777

2778
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
42✔
2779
        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex);
42✔
2780
        if ($extendsIndex === false) {
42✔
2781
            return false;
9✔
2782
        }
2783

2784
        $find = [
22✔
2785
            T_NS_SEPARATOR,
33✔
2786
            T_STRING,
33✔
2787
            T_WHITESPACE,
33✔
2788
        ];
22✔
2789

2790
        $end  = $this->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true);
33✔
2791
        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
33✔
2792
        $name = trim($name);
33✔
2793

2794
        if ($name === '') {
33✔
2795
            return false;
3✔
2796
        }
2797

2798
        return $name;
30✔
2799

2800
    }//end findExtendedClassName()
2801

2802

2803
    /**
2804
     * Returns the names of the interfaces that the specified class or enum implements.
2805
     *
2806
     * Returns FALSE on error or if there are no implemented interface names.
2807
     *
2808
     * @param int $stackPtr The stack position of the class or enum token.
2809
     *
2810
     * @return array|false
2811
     */
2812
    public function findImplementedInterfaceNames($stackPtr)
48✔
2813
    {
2814
        // Check for the existence of the token.
2815
        if (isset($this->tokens[$stackPtr]) === false) {
48✔
2816
            return false;
3✔
2817
        }
2818

2819
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
45✔
2820
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
45✔
2821
            && $this->tokens[$stackPtr]['code'] !== T_ENUM
45✔
2822
        ) {
2823
            return false;
6✔
2824
        }
2825

2826
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
39✔
2827
            return false;
3✔
2828
        }
2829

2830
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
36✔
2831
        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
36✔
2832
        if ($implementsIndex === false) {
36✔
2833
            return false;
6✔
2834
        }
2835

2836
        $find = [
20✔
2837
            T_NS_SEPARATOR,
30✔
2838
            T_STRING,
30✔
2839
            T_WHITESPACE,
30✔
2840
            T_COMMA,
30✔
2841
        ];
20✔
2842

2843
        $end  = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
30✔
2844
        $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
30✔
2845
        $name = trim($name);
30✔
2846

2847
        if ($name === '') {
30✔
2848
            return false;
3✔
2849
        } else {
2850
            $names = explode(',', $name);
27✔
2851
            $names = array_map('trim', $names);
27✔
2852
            return $names;
27✔
2853
        }
2854

2855
    }//end findImplementedInterfaceNames()
2856

2857

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

© 2026 Coveralls, Inc