Completed
Branch development (b1b115)
by Johannes
10:28
created

File::process()   F

Complexity

Conditions 49
Paths 1073

Size

Total Lines 226

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 226
rs 0
cc 49
nc 1073
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Represents a piece of content being checked during the run.
4
 *
5
 * @author    Greg Sherwood <[email protected]>
6
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9
10
namespace PHP_CodeSniffer\Files;
11
12
use PHP_CodeSniffer\Ruleset;
13
use PHP_CodeSniffer\Config;
14
use PHP_CodeSniffer\Fixer;
15
use PHP_CodeSniffer\Util;
16
use PHP_CodeSniffer\Exceptions\RuntimeException;
17
use PHP_CodeSniffer\Exceptions\TokenizerException;
18
19
class File
20
{
21
22
    /**
23
     * The absolute path to the file associated with this object.
24
     *
25
     * @var string
26
     */
27
    public $path = '';
28
29
    /**
30
     * The absolute path to the file associated with this object.
31
     *
32
     * @var string
33
     */
34
    protected $content = '';
35
36
    /**
37
     * The config data for the run.
38
     *
39
     * @var \PHP_CodeSniffer\Config
40
     */
41
    public $config = null;
42
43
    /**
44
     * The ruleset used for the run.
45
     *
46
     * @var \PHP_CodeSniffer\Ruleset
47
     */
48
    public $ruleset = null;
49
50
    /**
51
     * If TRUE, the entire file is being ignored.
52
     *
53
     * @var string
54
     */
55
    public $ignored = false;
56
57
    /**
58
     * The EOL character this file uses.
59
     *
60
     * @var string
61
     */
62
    public $eolChar = '';
63
64
    /**
65
     * The Fixer object to control fixing errors.
66
     *
67
     * @var \PHP_CodeSniffer\Fixer
68
     */
69
    public $fixer = null;
70
71
    /**
72
     * The tokenizer being used for this file.
73
     *
74
     * @var \PHP_CodeSniffer\Tokenizers\Tokenizer
75
     */
76
    public $tokenizer = null;
77
78
    /**
79
     * Was the file loaded from cache?
80
     *
81
     * If TRUE, the file was loaded from a local cache.
82
     * If FALSE, the file was tokenized and processed fully.
83
     *
84
     * @var boolean
85
     */
86
    public $fromCache = false;
87
88
    /**
89
     * The number of tokens in this file.
90
     *
91
     * Stored here to save calling count() everywhere.
92
     *
93
     * @var integer
94
     */
95
    public $numTokens = 0;
96
97
    /**
98
     * The tokens stack map.
99
     *
100
     * @var array
101
     */
102
    protected $tokens = [];
103
104
    /**
105
     * The errors raised from sniffs.
106
     *
107
     * @var array
108
     * @see getErrors()
109
     */
110
    protected $errors = [];
111
112
    /**
113
     * The warnings raised from sniffs.
114
     *
115
     * @var array
116
     * @see getWarnings()
117
     */
118
    protected $warnings = [];
119
120
    /**
121
     * The metrics recorded by sniffs.
122
     *
123
     * @var array
124
     * @see getMetrics()
125
     */
126
    protected $metrics = [];
127
128
    /**
129
     * The metrics recorded for each token.
130
     *
131
     * Stops the same metric being recorded for the same token twice.
132
     *
133
     * @var array
134
     * @see getMetrics()
135
     */
136
    private $metricTokens = [];
137
138
    /**
139
     * The total number of errors raised.
140
     *
141
     * @var integer
142
     */
143
    protected $errorCount = 0;
144
145
    /**
146
     * The total number of warnings raised.
147
     *
148
     * @var integer
149
     */
150
    protected $warningCount = 0;
151
152
    /**
153
     * The total number of errors and warnings that can be fixed.
154
     *
155
     * @var integer
156
     */
157
    protected $fixableCount = 0;
158
159
    /**
160
     * The total number of errors and warnings that were fixed.
161
     *
162
     * @var integer
163
     */
164
    protected $fixedCount = 0;
165
166
    /**
167
     * An array of sniffs that are being ignored.
168
     *
169
     * @var array
170
     */
171
    protected $ignoredListeners = [];
172
173
    /**
174
     * An array of message codes that are being ignored.
175
     *
176
     * @var array
177
     */
178
    protected $ignoredCodes = [];
179
180
    /**
181
     * An array of sniffs listening to this file's processing.
182
     *
183
     * @var \PHP_CodeSniffer\Sniffs\Sniff[]
184
     */
185
    protected $listeners = [];
186
187
    /**
188
     * The class name of the sniff currently processing the file.
189
     *
190
     * @var string
191
     */
192
    protected $activeListener = '';
193
194
    /**
195
     * An array of sniffs being processed and how long they took.
196
     *
197
     * @var array
198
     */
199
    protected $listenerTimes = [];
200
201
    /**
202
     * A cache of often used config settings to improve performance.
203
     *
204
     * Storing them here saves 10k+ calls to __get() in the Config class.
205
     *
206
     * @var array
207
     */
208
    protected $configCache = [];
209
210
211
    /**
212
     * Constructs a file.
213
     *
214
     * @param string                   $path    The absolute path to the file to process.
215
     * @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run.
216
     * @param \PHP_CodeSniffer\Config  $config  The config data for the run.
217
     *
218
     * @return void
219
     */
220
    public function __construct($path, Ruleset $ruleset, Config $config)
221
    {
222
        $this->path    = $path;
223
        $this->ruleset = $ruleset;
224
        $this->config  = $config;
225
        $this->fixer   = new Fixer();
226
227
        $parts     = explode('.', $path);
228
        $extension = array_pop($parts);
229
        if (isset($config->extensions[$extension]) === true) {
230
            $this->tokenizerType = $config->extensions[$extension];
231
        } else {
232
            // Revert to default.
233
            $this->tokenizerType = 'PHP';
234
        }
235
236
        $this->configCache['cache']           = $this->config->cache;
237
        $this->configCache['sniffs']          = array_map('strtolower', $this->config->sniffs);
238
        $this->configCache['exclude']         = array_map('strtolower', $this->config->exclude);
239
        $this->configCache['errorSeverity']   = $this->config->errorSeverity;
240
        $this->configCache['warningSeverity'] = $this->config->warningSeverity;
241
        $this->configCache['recordErrors']    = $this->config->recordErrors;
242
        $this->configCache['ignorePatterns']  = $this->ruleset->getIgnorePatterns();
243
244
    }//end __construct()
245
246
247
    /**
248
     * Set the content of the file.
249
     *
250
     * Setting the content also calculates the EOL char being used.
251
     *
252
     * @param string $content The file content.
253
     *
254
     * @return void
255
     */
256
    public function setContent($content)
257
    {
258
        $this->content = $content;
259
        $this->tokens  = [];
260
261
        try {
262
            $this->eolChar = Util\Common::detectLineEndings($content);
263
        } catch (RuntimeException $e) {
264
            $this->addWarningOnLine($e->getMessage(), 1, 'Internal.DetectLineEndings');
265
            return;
266
        }
267
268
    }//end setContent()
269
270
271
    /**
272
     * Reloads the content of the file.
273
     *
274
     * By default, we have no idea where our content comes from,
275
     * so we can't do anything.
276
     *
277
     * @return void
278
     */
279
    public function reloadContent()
280
    {
281
282
    }//end reloadContent()
283
284
285
    /**
286
     * Disables caching of this file.
287
     *
288
     * @return void
289
     */
290
    public function disableCaching()
291
    {
292
        $this->configCache['cache'] = false;
293
294
    }//end disableCaching()
295
296
297
    /**
298
     * Starts the stack traversal and tells listeners when tokens are found.
299
     *
300
     * @return void
301
     */
302
    public function process()
303
    {
304
        if ($this->ignored === true) {
305
            return;
306
        }
307
308
        $this->errors       = [];
309
        $this->warnings     = [];
310
        $this->errorCount   = 0;
311
        $this->warningCount = 0;
312
        $this->fixableCount = 0;
313
314
        $this->parse();
315
316
        $this->fixer->startFile($this);
317
318
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
319
            echo "\t*** START TOKEN PROCESSING ***".PHP_EOL;
320
        }
321
322
        $foundCode        = false;
323
        $listenerIgnoreTo = [];
324
        $inTests          = defined('PHP_CODESNIFFER_IN_TESTS');
325
        $checkAnnotations = $this->config->annotations;
326
327
        // Foreach of the listeners that have registered to listen for this
328
        // token, get them to process it.
329
        foreach ($this->tokens as $stackPtr => $token) {
330
            // Check for ignored lines.
331
            if ($checkAnnotations === true
332
                && ($token['code'] === T_COMMENT
333
                || $token['code'] === T_PHPCS_IGNORE_FILE
334
                || $token['code'] === T_PHPCS_SET
335
                || $token['code'] === T_DOC_COMMENT_STRING
336
                || $token['code'] === T_DOC_COMMENT_TAG
337
                || ($inTests === true && $token['code'] === T_INLINE_HTML))
338
            ) {
339
                $commentText      = ltrim($this->tokens[$stackPtr]['content'], ' /*');
340
                $commentTextLower = strtolower($commentText);
341
                if (strpos($commentText, '@codingStandards') !== false) {
342
                    if (strpos($commentText, '@codingStandardsIgnoreFile') !== false) {
343
                        // Ignoring the whole file, just a little late.
344
                        $this->errors       = [];
345
                        $this->warnings     = [];
346
                        $this->errorCount   = 0;
347
                        $this->warningCount = 0;
348
                        $this->fixableCount = 0;
349
                        return;
350
                    } else if (strpos($commentText, '@codingStandardsChangeSetting') !== false) {
351
                        $start   = strpos($commentText, '@codingStandardsChangeSetting');
352
                        $comment = substr($commentText, ($start + 30));
353
                        $parts   = explode(' ', $comment);
354
                        if ($parts >= 3) {
355
                            $sniffParts = explode('.', $parts[0]);
356
                            if ($sniffParts >= 3) {
357
                                // If the sniff code is not known to us, it has not been registered in this run.
358
                                // But don't throw an error as it could be there for a different standard to use.
359
                                if (isset($this->ruleset->sniffCodes[$parts[0]]) === true) {
360
                                    $listenerCode  = array_shift($parts);
361
                                    $propertyCode  = array_shift($parts);
362
                                    $propertyValue = rtrim(implode(' ', $parts), " */\r\n");
363
                                    $listenerClass = $this->ruleset->sniffCodes[$listenerCode];
364
                                    $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $propertyValue);
365
                                }
366
                            }
367
                        }
368
                    }//end if
369
                } else if (substr($commentTextLower, 0, 16) === 'phpcs:ignorefile') {
370
                    // Ignoring the whole file, just a little late.
371
                    $this->errors       = [];
372
                    $this->warnings     = [];
373
                    $this->errorCount   = 0;
374
                    $this->warningCount = 0;
375
                    $this->fixableCount = 0;
376
                    return;
377
                } else if (substr($commentTextLower, 0, 9) === 'phpcs:set') {
378
                    // Need to maintain case here, to get the correct sniff code.
379
                    $parts = explode(' ', substr($commentText, 10));
380
                    if ($parts >= 3) {
381
                        $sniffParts = explode('.', $parts[0]);
382
                        if ($sniffParts >= 3) {
383
                            // If the sniff code is not known to us, it has not been registered in this run.
384
                            // But don't throw an error as it could be there for a different standard to use.
385
                            if (isset($this->ruleset->sniffCodes[$parts[0]]) === true) {
386
                                $listenerCode  = array_shift($parts);
387
                                $propertyCode  = array_shift($parts);
388
                                $propertyValue = rtrim(implode(' ', $parts), " */\r\n");
389
                                $listenerClass = $this->ruleset->sniffCodes[$listenerCode];
390
                                $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $propertyValue);
391
                            }
392
                        }
393
                    }
394
                }//end if
395
            }//end if
396
397
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
398
                $type    = $token['type'];
399
                $content = Util\Common::prepareForOutput($token['content']);
400
                echo "\t\tProcess token $stackPtr: $type => $content".PHP_EOL;
401
            }
402
403
            if ($token['code'] !== T_INLINE_HTML) {
404
                $foundCode = true;
405
            }
406
407
            if (isset($this->ruleset->tokenListeners[$token['code']]) === false) {
408
                continue;
409
            }
410
411
            foreach ($this->ruleset->tokenListeners[$token['code']] as $listenerData) {
412
                if (isset($this->ignoredListeners[$listenerData['class']]) === true
413
                    || (isset($listenerIgnoreTo[$listenerData['class']]) === true
414
                    && $listenerIgnoreTo[$listenerData['class']] > $stackPtr)
415
                ) {
416
                    // This sniff is ignoring past this token, or the whole file.
417
                    continue;
418
                }
419
420
                // Make sure this sniff supports the tokenizer
421
                // we are currently using.
422
                $class = $listenerData['class'];
423
424
                if (isset($listenerData['tokenizers'][$this->tokenizerType]) === false) {
425
                    continue;
426
                }
427
428
                // If the file path matches one of our ignore patterns, skip it.
429
                // While there is support for a type of each pattern
430
                // (absolute or relative) we don't actually support it here.
431
                foreach ($listenerData['ignore'] as $pattern) {
432
                    // We assume a / directory separator, as do the exclude rules
433
                    // most developers write, so we need a special case for any system
434
                    // that is different.
435
                    if (DIRECTORY_SEPARATOR === '\\') {
436
                        $pattern = str_replace('/', '\\\\', $pattern);
437
                    }
438
439
                    $pattern = '`'.$pattern.'`i';
440
                    if (preg_match($pattern, $this->path) === 1) {
441
                        $this->ignoredListeners[$class] = true;
442
                        continue(2);
443
                    }
444
                }
445
446
                // If the file path does not match one of our include patterns, skip it.
447
                // While there is support for a type of each pattern
448
                // (absolute or relative) we don't actually support it here.
449
                if (empty($listenerData['include']) === false) {
450
                    $included = false;
451
                    foreach ($listenerData['include'] as $pattern) {
452
                        // We assume a / directory separator, as do the exclude rules
453
                        // most developers write, so we need a special case for any system
454
                        // that is different.
455
                        if (DIRECTORY_SEPARATOR === '\\') {
456
                            $pattern = str_replace('/', '\\\\', $pattern);
457
                        }
458
459
                        $pattern = '`'.$pattern.'`i';
460
                        if (preg_match($pattern, $this->path) === 1) {
461
                            $included = true;
462
                            break;
463
                        }
464
                    }
465
466
                    if ($included === false) {
467
                        $this->ignoredListeners[$class] = true;
468
                        continue;
469
                    }
470
                }//end if
471
472
                $this->activeListener = $class;
473
474
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
475
                    $startTime = microtime(true);
476
                    echo "\t\t\tProcessing ".$this->activeListener.'... ';
477
                }
478
479
                $ignoreTo = $this->ruleset->sniffs[$class]->process($this, $stackPtr);
480
                if ($ignoreTo !== null) {
481
                    $listenerIgnoreTo[$this->activeListener] = $ignoreTo;
482
                }
483
484
                if (PHP_CODESNIFFER_VERBOSITY > 2) {
485
                    $timeTaken = (microtime(true) - $startTime);
486
                    if (isset($this->listenerTimes[$this->activeListener]) === false) {
487
                        $this->listenerTimes[$this->activeListener] = 0;
488
                    }
489
490
                    $this->listenerTimes[$this->activeListener] += $timeTaken;
491
492
                    $timeTaken = round(($timeTaken), 4);
493
                    echo "DONE in $timeTaken seconds".PHP_EOL;
494
                }
495
496
                $this->activeListener = '';
497
            }//end foreach
498
        }//end foreach
499
500
        // If short open tags are off but the file being checked uses
501
        // short open tags, the whole content will be inline HTML
502
        // and nothing will be checked. So try and handle this case.
503
        // We don't show this error for STDIN because we can't be sure the content
504
        // actually came directly from the user. It could be something like
505
        // refs from a Git pre-push hook.
506
        if ($foundCode === false && $this->tokenizerType === 'PHP' && $this->path !== 'STDIN') {
507
            $shortTags = (bool) ini_get('short_open_tag');
508
            if ($shortTags === false) {
509
                $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.';
510
                $this->addWarning($error, null, 'Internal.NoCodeFound');
511
            }
512
        }
513
514
        if (PHP_CODESNIFFER_VERBOSITY > 2) {
515
            echo "\t*** END TOKEN PROCESSING ***".PHP_EOL;
516
            echo "\t*** START SNIFF PROCESSING REPORT ***".PHP_EOL;
517
518
            asort($this->listenerTimes, SORT_NUMERIC);
519
            $this->listenerTimes = array_reverse($this->listenerTimes, true);
520
            foreach ($this->listenerTimes as $listener => $timeTaken) {
521
                echo "\t$listener: ".round(($timeTaken), 4).' secs'.PHP_EOL;
522
            }
523
524
            echo "\t*** END SNIFF PROCESSING REPORT ***".PHP_EOL;
525
        }
526
527
        $this->fixedCount += $this->fixer->getFixCount();
528
529
    }//end process()
530
531
532
    /**
533
     * Tokenizes the file and prepares it for the test run.
534
     *
535
     * @return void
536
     */
537
    public function parse()
538
    {
539
        if (empty($this->tokens) === false) {
540
            // File has already been parsed.
541
            return;
542
        }
543
544
        try {
545
            $tokenizerClass  = 'PHP_CodeSniffer\Tokenizers\\'.$this->tokenizerType;
546
            $this->tokenizer = new $tokenizerClass($this->content, $this->config, $this->eolChar);
547
            $this->tokens    = $this->tokenizer->getTokens();
548
        } catch (TokenizerException $e) {
549
            $this->addWarning($e->getMessage(), null, 'Internal.Tokenizer.Exception');
550
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
551
                echo "[$this->tokenizerType => tokenizer error]... ";
552
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
553
                    echo PHP_EOL;
554
                }
555
            }
556
557
            return;
558
        }
559
560
        $this->numTokens = count($this->tokens);
561
562
        // Check for mixed line endings as these can cause tokenizer errors and we
563
        // should let the user know that the results they get may be incorrect.
564
        // This is done by removing all backslashes, removing the newline char we
565
        // detected, then converting newlines chars into text. If any backslashes
566
        // are left at the end, we have additional newline chars in use.
567
        $contents = str_replace('\\', '', $this->content);
568
        $contents = str_replace($this->eolChar, '', $contents);
569
        $contents = str_replace("\n", '\n', $contents);
570
        $contents = str_replace("\r", '\r', $contents);
571
        if (strpos($contents, '\\') !== false) {
572
            $error = 'File has mixed line endings; this may cause incorrect results';
573
            $this->addWarningOnLine($error, 1, 'Internal.LineEndings.Mixed');
574
        }
575
576
        if (PHP_CODESNIFFER_VERBOSITY > 0) {
577
            if ($this->numTokens === 0) {
578
                $numLines = 0;
579
            } else {
580
                $numLines = $this->tokens[($this->numTokens - 1)]['line'];
581
            }
582
583
            echo "[$this->tokenizerType => $this->numTokens tokens in $numLines lines]... ";
584
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
585
                echo PHP_EOL;
586
            }
587
        }
588
589
    }//end parse()
590
591
592
    /**
593
     * Returns the token stack for this file.
594
     *
595
     * @return array
596
     */
597
    public function getTokens()
598
    {
599
        return $this->tokens;
600
601
    }//end getTokens()
602
603
604
    /**
605
     * Remove vars stored in this file that are no longer required.
606
     *
607
     * @return void
608
     */
609
    public function cleanUp()
610
    {
611
        $this->listenerTimes = null;
612
        $this->content       = null;
613
        $this->tokens        = null;
614
        $this->metricTokens  = null;
615
        $this->tokenizer     = null;
616
        $this->fixer         = null;
617
        $this->config        = null;
618
        $this->ruleset       = null;
619
620
    }//end cleanUp()
621
622
623
    /**
624
     * Records an error against a specific token in the file.
625
     *
626
     * @param string  $error    The error message.
627
     * @param int     $stackPtr The stack position where the error occurred.
628
     * @param string  $code     A violation code unique to the sniff message.
629
     * @param array   $data     Replacements for the error message.
630
     * @param int     $severity The severity level for this error. A value of 0
631
     *                          will be converted into the default severity level.
632
     * @param boolean $fixable  Can the error be fixed by the sniff?
633
     *
634
     * @return boolean
635
     */
636
    public function addError(
637
        $error,
638
        $stackPtr,
639
        $code,
640
        $data=[],
641
        $severity=0,
642
        $fixable=false
643
    ) {
644
        if ($stackPtr === null) {
645
            $line   = 1;
646
            $column = 1;
647
        } else {
648
            $line   = $this->tokens[$stackPtr]['line'];
649
            $column = $this->tokens[$stackPtr]['column'];
650
        }
651
652
        return $this->addMessage(true, $error, $line, $column, $code, $data, $severity, $fixable);
653
654
    }//end addError()
655
656
657
    /**
658
     * Records a warning against a specific token in the file.
659
     *
660
     * @param string  $warning  The error message.
661
     * @param int     $stackPtr The stack position where the error occurred.
662
     * @param string  $code     A violation code unique to the sniff message.
663
     * @param array   $data     Replacements for the warning message.
664
     * @param int     $severity The severity level for this warning. A value of 0
665
     *                          will be converted into the default severity level.
666
     * @param boolean $fixable  Can the warning be fixed by the sniff?
667
     *
668
     * @return boolean
669
     */
670
    public function addWarning(
671
        $warning,
672
        $stackPtr,
673
        $code,
674
        $data=[],
675
        $severity=0,
676
        $fixable=false
677
    ) {
678
        if ($stackPtr === null) {
679
            $line   = 1;
680
            $column = 1;
681
        } else {
682
            $line   = $this->tokens[$stackPtr]['line'];
683
            $column = $this->tokens[$stackPtr]['column'];
684
        }
685
686
        return $this->addMessage(false, $warning, $line, $column, $code, $data, $severity, $fixable);
687
688
    }//end addWarning()
689
690
691
    /**
692
     * Records an error against a specific line in the file.
693
     *
694
     * @param string $error    The error message.
695
     * @param int    $line     The line on which the error occurred.
696
     * @param string $code     A violation code unique to the sniff message.
697
     * @param array  $data     Replacements for the error message.
698
     * @param int    $severity The severity level for this error. A value of 0
699
     *                         will be converted into the default severity level.
700
     *
701
     * @return boolean
702
     */
703
    public function addErrorOnLine(
704
        $error,
705
        $line,
706
        $code,
707
        $data=[],
708
        $severity=0
709
    ) {
710
        return $this->addMessage(true, $error, $line, 1, $code, $data, $severity, false);
711
712
    }//end addErrorOnLine()
713
714
715
    /**
716
     * Records a warning against a specific token in the file.
717
     *
718
     * @param string $warning  The error message.
719
     * @param int    $line     The line on which the warning occurred.
720
     * @param string $code     A violation code unique to the sniff message.
721
     * @param array  $data     Replacements for the warning message.
722
     * @param int    $severity The severity level for this warning. A value of 0 will
723
     *                         will be converted into the default severity level.
724
     *
725
     * @return boolean
726
     */
727
    public function addWarningOnLine(
728
        $warning,
729
        $line,
730
        $code,
731
        $data=[],
732
        $severity=0
733
    ) {
734
        return $this->addMessage(false, $warning, $line, 1, $code, $data, $severity, false);
735
736
    }//end addWarningOnLine()
737
738
739
    /**
740
     * Records a fixable error against a specific token in the file.
741
     *
742
     * Returns true if the error was recorded and should be fixed.
743
     *
744
     * @param string $error    The error message.
745
     * @param int    $stackPtr The stack position where the error occurred.
746
     * @param string $code     A violation code unique to the sniff message.
747
     * @param array  $data     Replacements for the error message.
748
     * @param int    $severity The severity level for this error. A value of 0
749
     *                         will be converted into the default severity level.
750
     *
751
     * @return boolean
752
     */
753
    public function addFixableError(
754
        $error,
755
        $stackPtr,
756
        $code,
757
        $data=[],
758
        $severity=0
759
    ) {
760
        $recorded = $this->addError($error, $stackPtr, $code, $data, $severity, true);
761
        if ($recorded === true && $this->fixer->enabled === true) {
762
            return true;
763
        }
764
765
        return false;
766
767
    }//end addFixableError()
768
769
770
    /**
771
     * Records a fixable warning against a specific token in the file.
772
     *
773
     * Returns true if the warning was recorded and should be fixed.
774
     *
775
     * @param string $warning  The error message.
776
     * @param int    $stackPtr The stack position where the error occurred.
777
     * @param string $code     A violation code unique to the sniff message.
778
     * @param array  $data     Replacements for the warning message.
779
     * @param int    $severity The severity level for this warning. A value of 0
780
     *                         will be converted into the default severity level.
781
     *
782
     * @return boolean
783
     */
784
    public function addFixableWarning(
785
        $warning,
786
        $stackPtr,
787
        $code,
788
        $data=[],
789
        $severity=0
790
    ) {
791
        $recorded = $this->addWarning($warning, $stackPtr, $code, $data, $severity, true);
792
        if ($recorded === true && $this->fixer->enabled === true) {
793
            return true;
794
        }
795
796
        return false;
797
798
    }//end addFixableWarning()
799
800
801
    /**
802
     * Adds an error to the error stack.
803
     *
804
     * @param boolean $error    Is this an error message?
805
     * @param string  $message  The text of the message.
806
     * @param int     $line     The line on which the message occurred.
807
     * @param int     $column   The column at which the message occurred.
808
     * @param string  $code     A violation code unique to the sniff message.
809
     * @param array   $data     Replacements for the message.
810
     * @param int     $severity The severity level for this message. A value of 0
811
     *                          will be converted into the default severity level.
812
     * @param boolean $fixable  Can the problem be fixed by the sniff?
813
     *
814
     * @return boolean
815
     */
816
    protected function addMessage($error, $message, $line, $column, $code, $data, $severity, $fixable)
817
    {
818
        // Check if this line is ignoring all message codes.
819
        if (isset($this->tokenizer->ignoredLines[$line]['all']) === true) {
820
            return false;
821
        }
822
823
        // Work out which sniff generated the message.
824
        $parts = explode('.', $code);
825
        if ($parts[0] === 'Internal') {
826
            // An internal message.
827
            $listenerCode = Util\Common::getSniffCode($this->activeListener);
828
            $sniffCode    = $code;
829
            $checkCodes   = [$sniffCode];
830
        } else {
831
            if ($parts[0] !== $code) {
832
                // The full message code has been passed in.
833
                $sniffCode    = $code;
834
                $listenerCode = substr($sniffCode, 0, strrpos($sniffCode, '.'));
835
            } else {
836
                $listenerCode = Util\Common::getSniffCode($this->activeListener);
837
                $sniffCode    = $listenerCode.'.'.$code;
838
                $parts        = explode('.', $sniffCode);
839
            }
840
841
            $checkCodes = [
842
                $sniffCode,
843
                $parts[0].'.'.$parts[1].'.'.$parts[2],
844
                $parts[0].'.'.$parts[1],
845
                $parts[0],
846
            ];
847
        }//end if
848
849
        // Check if this line is ignoring this specific message.
850
        foreach ($checkCodes as $checkCode) {
851
            if (isset($this->tokenizer->ignoredLines[$line][$checkCode]) === true) {
852
                return false;
853
            }
854
        }
855
856
        $includeAll = true;
857
        if ($this->configCache['cache'] === false
858
            || $this->configCache['recordErrors'] === false
859
        ) {
860
            $includeAll = false;
861
        }
862
863
        // Filter out any messages for sniffs that shouldn't have run
864
        // due to the use of the --sniffs command line argument.
865
        if ($includeAll === false
866
            && ((empty($this->configCache['sniffs']) === false
867
            && in_array(strtolower($listenerCode), $this->configCache['sniffs']) === false)
868
            || (empty($this->configCache['exclude']) === false
869
            && in_array(strtolower($listenerCode), $this->configCache['exclude']) === true))
870
        ) {
871
            return false;
872
        }
873
874
        // If we know this sniff code is being ignored for this file, return early.
875
        foreach ($checkCodes as $checkCode) {
876
            if (isset($this->ignoredCodes[$checkCode]) === true) {
877
                return false;
878
            }
879
        }
880
881
        $oppositeType = 'warning';
882
        if ($error === false) {
883
            $oppositeType = 'error';
884
        }
885
886
        foreach ($checkCodes as $checkCode) {
887
            // Make sure this message type has not been set to the opposite message type.
888
            if (isset($this->ruleset->ruleset[$checkCode]['type']) === true
889
                && $this->ruleset->ruleset[$checkCode]['type'] === $oppositeType
890
            ) {
891
                $error = !$error;
892
                break;
893
            }
894
        }
895
896
        if ($error === true) {
897
            $configSeverity = $this->configCache['errorSeverity'];
898
            $messageCount   = &$this->errorCount;
899
            $messages       = &$this->errors;
900
        } else {
901
            $configSeverity = $this->configCache['warningSeverity'];
902
            $messageCount   = &$this->warningCount;
903
            $messages       = &$this->warnings;
904
        }
905
906
        if ($includeAll === false && $configSeverity === 0) {
907
            // Don't bother doing any processing as these messages are just going to
908
            // be hidden in the reports anyway.
909
            return false;
910
        }
911
912
        if ($severity === 0) {
913
            $severity = 5;
914
        }
915
916
        foreach ($checkCodes as $checkCode) {
917
            // Make sure we are interested in this severity level.
918
            if (isset($this->ruleset->ruleset[$checkCode]['severity']) === true) {
919
                $severity = $this->ruleset->ruleset[$checkCode]['severity'];
920
                break;
921
            }
922
        }
923
924
        if ($includeAll === false && $configSeverity > $severity) {
925
            return false;
926
        }
927
928
        // Make sure we are not ignoring this file.
929
        foreach ($checkCodes as $checkCode) {
930
            if (isset($this->configCache['ignorePatterns'][$checkCode]) === false) {
931
                continue;
932
            }
933
934
            foreach ($this->configCache['ignorePatterns'][$checkCode] as $pattern => $type) {
935
                // While there is support for a type of each pattern
936
                // (absolute or relative) we don't actually support it here.
937
                $replacements = [
938
                    '\\,' => ',',
939
                    '*'   => '.*',
940
                ];
941
942
                // We assume a / directory separator, as do the exclude rules
943
                // most developers write, so we need a special case for any system
944
                // that is different.
945
                if (DIRECTORY_SEPARATOR === '\\') {
946
                    $replacements['/'] = '\\\\';
947
                }
948
949
                $pattern = '`'.strtr($pattern, $replacements).'`i';
950
                if (preg_match($pattern, $this->path) === 1) {
951
                    $this->ignoredCodes[$checkCode] = true;
952
                    return false;
953
                }
954
            }//end foreach
955
        }//end foreach
956
957
        $messageCount++;
958
        if ($fixable === true) {
959
            $this->fixableCount++;
960
        }
961
962
        if ($this->configCache['recordErrors'] === false
963
            && $includeAll === false
964
        ) {
965
            return true;
966
        }
967
968
        // Work out the error message.
969
        if (isset($this->ruleset->ruleset[$sniffCode]['message']) === true) {
970
            $message = $this->ruleset->ruleset[$sniffCode]['message'];
971
        }
972
973
        if (empty($data) === false) {
974
            $message = vsprintf($message, $data);
975
        }
976
977
        if (isset($messages[$line]) === false) {
978
            $messages[$line] = [];
979
        }
980
981
        if (isset($messages[$line][$column]) === false) {
982
            $messages[$line][$column] = [];
983
        }
984
985
        $messages[$line][$column][] = [
986
            'message'  => $message,
987
            'source'   => $sniffCode,
988
            'listener' => $this->activeListener,
989
            'severity' => $severity,
990
            'fixable'  => $fixable,
991
        ];
992
993
        if (PHP_CODESNIFFER_VERBOSITY > 1
994
            && $this->fixer->enabled === true
995
            && $fixable === true
996
        ) {
997
            @ob_end_clean();
998
            echo "\tE: [Line $line] $message ($sniffCode)".PHP_EOL;
999
            ob_start();
1000
        }
1001
1002
        return true;
1003
1004
    }//end addMessage()
1005
1006
1007
    /**
1008
     * Adds an warning to the warning stack.
1009
     *
1010
     * @param int    $stackPtr The stack position where the metric was recorded.
1011
     * @param string $metric   The name of the metric being recorded.
1012
     * @param string $value    The value of the metric being recorded.
1013
     *
1014
     * @return boolean
1015
     */
1016
    public function recordMetric($stackPtr, $metric, $value)
1017
    {
1018
        if (isset($this->metrics[$metric]) === false) {
1019
            $this->metrics[$metric] = ['values' => [$value => 1]];
1020
            $this->metricTokens[$metric][$stackPtr] = true;
1021
        } else if (isset($this->metricTokens[$metric][$stackPtr]) === false) {
1022
            $this->metricTokens[$metric][$stackPtr] = true;
1023
            if (isset($this->metrics[$metric]['values'][$value]) === false) {
1024
                $this->metrics[$metric]['values'][$value] = 1;
1025
            } else {
1026
                $this->metrics[$metric]['values'][$value]++;
1027
            }
1028
        }
1029
1030
        return true;
1031
1032
    }//end recordMetric()
1033
1034
1035
    /**
1036
     * Returns the number of errors raised.
1037
     *
1038
     * @return int
1039
     */
1040
    public function getErrorCount()
1041
    {
1042
        return $this->errorCount;
1043
1044
    }//end getErrorCount()
1045
1046
1047
    /**
1048
     * Returns the number of warnings raised.
1049
     *
1050
     * @return int
1051
     */
1052
    public function getWarningCount()
1053
    {
1054
        return $this->warningCount;
1055
1056
    }//end getWarningCount()
1057
1058
1059
    /**
1060
     * Returns the number of successes recorded.
1061
     *
1062
     * @return int
1063
     */
1064
    public function getSuccessCount()
1065
    {
1066
        return $this->successCount;
1067
1068
    }//end getSuccessCount()
1069
1070
1071
    /**
1072
     * Returns the number of fixable errors/warnings raised.
1073
     *
1074
     * @return int
1075
     */
1076
    public function getFixableCount()
1077
    {
1078
        return $this->fixableCount;
1079
1080
    }//end getFixableCount()
1081
1082
1083
    /**
1084
     * Returns the number of fixed errors/warnings.
1085
     *
1086
     * @return int
1087
     */
1088
    public function getFixedCount()
1089
    {
1090
        return $this->fixedCount;
1091
1092
    }//end getFixedCount()
1093
1094
1095
    /**
1096
     * Returns the list of ignored lines.
1097
     *
1098
     * @return array
1099
     */
1100
    public function getIgnoredLines()
1101
    {
1102
        return $this->tokenizer->ignoredLines;
1103
1104
    }//end getIgnoredLines()
1105
1106
1107
    /**
1108
     * Returns the errors raised from processing this file.
1109
     *
1110
     * @return array
1111
     */
1112
    public function getErrors()
1113
    {
1114
        return $this->errors;
1115
1116
    }//end getErrors()
1117
1118
1119
    /**
1120
     * Returns the warnings raised from processing this file.
1121
     *
1122
     * @return array
1123
     */
1124
    public function getWarnings()
1125
    {
1126
        return $this->warnings;
1127
1128
    }//end getWarnings()
1129
1130
1131
    /**
1132
     * Returns the metrics found while processing this file.
1133
     *
1134
     * @return array
1135
     */
1136
    public function getMetrics()
1137
    {
1138
        return $this->metrics;
1139
1140
    }//end getMetrics()
1141
1142
1143
    /**
1144
     * Returns the absolute filename of this file.
1145
     *
1146
     * @return string
1147
     */
1148
    public function getFilename()
1149
    {
1150
        return $this->path;
1151
1152
    }//end getFilename()
1153
1154
1155
    /**
1156
     * Returns the declaration names for classes, interfaces, traits, and functions.
1157
     *
1158
     * @param int $stackPtr The position of the declaration token which
1159
     *                      declared the class, interface, trait, or function.
1160
     *
1161
     * @return string|null The name of the class, interface, trait, or function;
1162
     *                     or NULL if the function or class is anonymous.
1163
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type
1164
     *                                                      T_FUNCTION, T_CLASS, T_ANON_CLASS,
1165
     *                                                      T_CLOSURE, T_TRAIT, or T_INTERFACE.
1166
     */
1167
    public function getDeclarationName($stackPtr)
1168
    {
1169
        $tokenCode = $this->tokens[$stackPtr]['code'];
1170
1171
        if ($tokenCode === T_ANON_CLASS || $tokenCode === T_CLOSURE) {
1172
            return null;
1173
        }
1174
1175
        if ($tokenCode !== T_FUNCTION
1176
            && $tokenCode !== T_CLASS
1177
            && $tokenCode !== T_INTERFACE
1178
            && $tokenCode !== T_TRAIT
1179
        ) {
1180
            throw new RuntimeException('Token type "'.$this->tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT');
1181
        }
1182
1183
        if ($tokenCode === T_FUNCTION
1184
            && strtolower($this->tokens[$stackPtr]['content']) !== 'function'
1185
        ) {
1186
            // This is a function declared without the "function" keyword.
1187
            // So this token is the function name.
1188
            return $this->tokens[$stackPtr]['content'];
1189
        }
1190
1191
        $content = null;
1192
        for ($i = $stackPtr; $i < $this->numTokens; $i++) {
1193
            if ($this->tokens[$i]['code'] === T_STRING) {
1194
                $content = $this->tokens[$i]['content'];
1195
                break;
1196
            }
1197
        }
1198
1199
        return $content;
1200
1201
    }//end getDeclarationName()
1202
1203
1204
    /**
1205
     * Returns the method parameters for the specified function token.
1206
     *
1207
     * Each parameter is in the following format:
1208
     *
1209
     * <code>
1210
     *   0 => array(
1211
     *         'name'              => '$var',  // The variable name.
1212
     *         'token'             => integer, // The stack pointer to the variable name.
1213
     *         'content'           => string,  // The full content of the variable definition.
1214
     *         'pass_by_reference' => boolean, // Is the variable passed by reference?
1215
     *         'variable_length'   => boolean, // Is the param of variable length through use of `...` ?
1216
     *         'type_hint'         => string,  // The type hint for the variable.
1217
     *         'nullable_type'     => boolean, // Is the variable using a nullable type?
1218
     *        )
1219
     * </code>
1220
     *
1221
     * Parameters with default values have an additional array index of
1222
     * 'default' with the value of the default as a string.
1223
     *
1224
     * @param int $stackPtr The position in the stack of the function token
1225
     *                      to acquire the parameters for.
1226
     *
1227
     * @return array
1228
     * @throws \PHP_CodeSniffer\Exceptions\TokenizerException If the specified $stackPtr is not of
1229
     *                                                        type T_FUNCTION or T_CLOSURE.
1230
     */
1231
    public function getMethodParameters($stackPtr)
1232
    {
1233
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
1234
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
1235
        ) {
1236
            throw new TokenizerException('$stackPtr must be of type T_FUNCTION or T_CLOSURE');
1237
        }
1238
1239
        $opener = $this->tokens[$stackPtr]['parenthesis_opener'];
1240
        $closer = $this->tokens[$stackPtr]['parenthesis_closer'];
1241
1242
        $vars            = [];
1243
        $currVar         = null;
1244
        $paramStart      = ($opener + 1);
1245
        $defaultStart    = null;
1246
        $paramCount      = 0;
1247
        $passByReference = false;
1248
        $variableLength  = false;
1249
        $typeHint        = '';
1250
        $nullableType    = false;
1251
1252
        for ($i = $paramStart; $i <= $closer; $i++) {
1253
            // Check to see if this token has a parenthesis or bracket opener. If it does
1254
            // it's likely to be an array which might have arguments in it. This
1255
            // could cause problems in our parsing below, so lets just skip to the
1256
            // end of it.
1257
            if (isset($this->tokens[$i]['parenthesis_opener']) === true) {
1258
                // Don't do this if it's the close parenthesis for the method.
1259
                if ($i !== $this->tokens[$i]['parenthesis_closer']) {
1260
                    $i = ($this->tokens[$i]['parenthesis_closer'] + 1);
1261
                }
1262
            }
1263
1264
            if (isset($this->tokens[$i]['bracket_opener']) === true) {
1265
                // Don't do this if it's the close parenthesis for the method.
1266
                if ($i !== $this->tokens[$i]['bracket_closer']) {
1267
                    $i = ($this->tokens[$i]['bracket_closer'] + 1);
1268
                }
1269
            }
1270
1271
            switch ($this->tokens[$i]['code']) {
1272
            case T_BITWISE_AND:
1273
                if ($defaultStart === null) {
1274
                    $passByReference = true;
1275
                }
1276
                break;
1277
            case T_VARIABLE:
1278
                $currVar = $i;
1279
                break;
1280
            case T_ELLIPSIS:
1281
                $variableLength = true;
1282
                break;
1283
            case T_ARRAY_HINT:
1284
            case T_CALLABLE:
1285
                $typeHint .= $this->tokens[$i]['content'];
1286
                break;
1287
            case T_SELF:
1288
            case T_PARENT:
1289
            case T_STATIC:
1290
                // Self is valid, the others invalid, but were probably intended as type hints.
1291
                if (isset($defaultStart) === false) {
1292
                    $typeHint .= $this->tokens[$i]['content'];
1293
                }
1294
                break;
1295
            case T_STRING:
1296
                // This is a string, so it may be a type hint, but it could
1297
                // also be a constant used as a default value.
1298
                $prevComma = false;
1299
                for ($t = $i; $t >= $opener; $t--) {
1300
                    if ($this->tokens[$t]['code'] === T_COMMA) {
1301
                        $prevComma = $t;
1302
                        break;
1303
                    }
1304
                }
1305
1306
                if ($prevComma !== false) {
1307
                    $nextEquals = false;
1308
                    for ($t = $prevComma; $t < $i; $t++) {
1309
                        if ($this->tokens[$t]['code'] === T_EQUAL) {
1310
                            $nextEquals = $t;
1311
                            break;
1312
                        }
1313
                    }
1314
1315
                    if ($nextEquals !== false) {
1316
                        break;
1317
                    }
1318
                }
1319
1320
                if ($defaultStart === null) {
1321
                    $typeHint .= $this->tokens[$i]['content'];
1322
                }
1323
                break;
1324
            case T_NS_SEPARATOR:
1325
                // Part of a type hint or default value.
1326
                if ($defaultStart === null) {
1327
                    $typeHint .= $this->tokens[$i]['content'];
1328
                }
1329
                break;
1330
            case T_NULLABLE:
1331
                if ($defaultStart === null) {
1332
                    $nullableType = true;
1333
                    $typeHint    .= $this->tokens[$i]['content'];
1334
                }
1335
                break;
1336
            case T_CLOSE_PARENTHESIS:
1337
            case T_COMMA:
1338
                // If it's null, then there must be no parameters for this
1339
                // method.
1340
                if ($currVar === null) {
1341
                    continue;
1342
                }
1343
1344
                $vars[$paramCount]            = [];
1345
                $vars[$paramCount]['token']   = $currVar;
1346
                $vars[$paramCount]['name']    = $this->tokens[$currVar]['content'];
1347
                $vars[$paramCount]['content'] = trim($this->getTokensAsString($paramStart, ($i - $paramStart)));
1348
1349
                if ($defaultStart !== null) {
1350
                    $vars[$paramCount]['default'] = trim($this->getTokensAsString($defaultStart, ($i - $defaultStart)));
1351
                }
1352
1353
                $vars[$paramCount]['pass_by_reference'] = $passByReference;
1354
                $vars[$paramCount]['variable_length']   = $variableLength;
1355
                $vars[$paramCount]['type_hint']         = $typeHint;
1356
                $vars[$paramCount]['nullable_type']     = $nullableType;
1357
1358
                // Reset the vars, as we are about to process the next parameter.
1359
                $defaultStart    = null;
1360
                $paramStart      = ($i + 1);
1361
                $passByReference = false;
1362
                $variableLength  = false;
1363
                $typeHint        = '';
1364
                $nullableType    = false;
1365
1366
                $paramCount++;
1367
                break;
1368
            case T_EQUAL:
1369
                $defaultStart = ($i + 1);
1370
                break;
1371
            }//end switch
1372
        }//end for
1373
1374
        return $vars;
1375
1376
    }//end getMethodParameters()
1377
1378
1379
    /**
1380
     * Returns the visibility and implementation properties of a method.
1381
     *
1382
     * The format of the array is:
1383
     * <code>
1384
     *   array(
1385
     *    'scope'           => 'public', // public protected or protected
1386
     *    'scope_specified' => true,     // true is scope keyword was found.
1387
     *    'is_abstract'     => false,    // true if the abstract keyword was found.
1388
     *    'is_final'        => false,    // true if the final keyword was found.
1389
     *    'is_static'       => false,    // true if the static keyword was found.
1390
     *   );
1391
     * </code>
1392
     *
1393
     * @param int $stackPtr The position in the stack of the function token to
1394
     *                      acquire the properties for.
1395
     *
1396
     * @return array
1397
     * @throws \PHP_CodeSniffer\Exceptions\TokenizerException If the specified position is not a
1398
     *                                                        T_FUNCTION token.
1399
     */
1400
    public function getMethodProperties($stackPtr)
1401
    {
1402
        if ($this->tokens[$stackPtr]['code'] !== T_FUNCTION
1403
            && $this->tokens[$stackPtr]['code'] !== T_CLOSURE
1404
        ) {
1405
            throw new TokenizerException('$stackPtr must be of type T_FUNCTION or T_CLOSURE');
1406
        }
1407
1408
        if ($this->tokens[$stackPtr]['code'] === T_FUNCTION) {
1409
            $valid = [
1410
                T_PUBLIC      => T_PUBLIC,
1411
                T_PRIVATE     => T_PRIVATE,
1412
                T_PROTECTED   => T_PROTECTED,
1413
                T_STATIC      => T_STATIC,
1414
                T_FINAL       => T_FINAL,
1415
                T_ABSTRACT    => T_ABSTRACT,
1416
                T_WHITESPACE  => T_WHITESPACE,
1417
                T_COMMENT     => T_COMMENT,
1418
                T_DOC_COMMENT => T_DOC_COMMENT,
1419
            ];
1420
        } else {
1421
            $valid = [
1422
                T_STATIC      => T_STATIC,
1423
                T_WHITESPACE  => T_WHITESPACE,
1424
                T_COMMENT     => T_COMMENT,
1425
                T_DOC_COMMENT => T_DOC_COMMENT,
1426
            ];
1427
        }
1428
1429
        $scope          = 'public';
1430
        $scopeSpecified = false;
1431
        $isAbstract     = false;
1432
        $isFinal        = false;
1433
        $isStatic       = false;
1434
1435
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
1436
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
1437
                break;
1438
            }
1439
1440
            switch ($this->tokens[$i]['code']) {
1441
            case T_PUBLIC:
1442
                $scope          = 'public';
1443
                $scopeSpecified = true;
1444
                break;
1445
            case T_PRIVATE:
1446
                $scope          = 'private';
1447
                $scopeSpecified = true;
1448
                break;
1449
            case T_PROTECTED:
1450
                $scope          = 'protected';
1451
                $scopeSpecified = true;
1452
                break;
1453
            case T_ABSTRACT:
1454
                $isAbstract = true;
1455
                break;
1456
            case T_FINAL:
1457
                $isFinal = true;
1458
                break;
1459
            case T_STATIC:
1460
                $isStatic = true;
1461
                break;
1462
            }//end switch
1463
        }//end for
1464
1465
        return [
1466
            'scope'           => $scope,
1467
            'scope_specified' => $scopeSpecified,
1468
            'is_abstract'     => $isAbstract,
1469
            'is_final'        => $isFinal,
1470
            'is_static'       => $isStatic,
1471
        ];
1472
1473
    }//end getMethodProperties()
1474
1475
1476
    /**
1477
     * Returns the visibility and implementation properties of the class member
1478
     * variable found at the specified position in the stack.
1479
     *
1480
     * The format of the array is:
1481
     *
1482
     * <code>
1483
     *   array(
1484
     *    'scope'       => 'public', // public protected or protected
1485
     *    'is_static'   => false,    // true if the static keyword was found.
1486
     *   );
1487
     * </code>
1488
     *
1489
     * @param int $stackPtr The position in the stack of the T_VARIABLE token to
1490
     *                      acquire the properties for.
1491
     *
1492
     * @return array
1493
     * @throws \PHP_CodeSniffer\Exceptions\TokenizerException If the specified position is not a
1494
     *                                                        T_VARIABLE token, or if the position is not
1495
     *                                                        a class member variable.
1496
     */
1497
    public function getMemberProperties($stackPtr)
1498
    {
1499
        if ($this->tokens[$stackPtr]['code'] !== T_VARIABLE) {
1500
            throw new TokenizerException('$stackPtr must be of type T_VARIABLE');
1501
        }
1502
1503
        $conditions = array_keys($this->tokens[$stackPtr]['conditions']);
1504
        $ptr        = array_pop($conditions);
1505
        if (isset($this->tokens[$ptr]) === false
1506
            || ($this->tokens[$ptr]['code'] !== T_CLASS
1507
            && $this->tokens[$ptr]['code'] !== T_ANON_CLASS
1508
            && $this->tokens[$ptr]['code'] !== T_TRAIT)
1509
        ) {
1510
            if (isset($this->tokens[$ptr]) === true
1511
                && $this->tokens[$ptr]['code'] === T_INTERFACE
1512
            ) {
1513
                // T_VARIABLEs in interfaces can actually be method arguments
1514
                // but they wont be seen as being inside the method because there
1515
                // are no scope openers and closers for abstract methods. If it is in
1516
                // parentheses, we can be pretty sure it is a method argument.
1517
                if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === false
1518
                    || empty($this->tokens[$stackPtr]['nested_parenthesis']) === true
1519
                ) {
1520
                    $error = 'Possible parse error: interfaces may not include member vars';
1521
                    $this->addWarning($error, $stackPtr, 'Internal.ParseError.InterfaceHasMemberVar');
1522
                    return [];
1523
                }
1524
            } else {
1525
                throw new TokenizerException('$stackPtr is not a class member var');
1526
            }
1527
        }
1528
1529
        $valid = [
1530
            T_PUBLIC      => T_PUBLIC,
1531
            T_PRIVATE     => T_PRIVATE,
1532
            T_PROTECTED   => T_PROTECTED,
1533
            T_STATIC      => T_STATIC,
1534
            T_WHITESPACE  => T_WHITESPACE,
1535
            T_COMMENT     => T_COMMENT,
1536
            T_DOC_COMMENT => T_DOC_COMMENT,
1537
            T_VARIABLE    => T_VARIABLE,
1538
            T_COMMA       => T_COMMA,
1539
        ];
1540
1541
        $scope          = 'public';
1542
        $scopeSpecified = false;
1543
        $isStatic       = false;
1544
1545
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
1546
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
1547
                break;
1548
            }
1549
1550
            switch ($this->tokens[$i]['code']) {
1551
            case T_PUBLIC:
1552
                $scope          = 'public';
1553
                $scopeSpecified = true;
1554
                break;
1555
            case T_PRIVATE:
1556
                $scope          = 'private';
1557
                $scopeSpecified = true;
1558
                break;
1559
            case T_PROTECTED:
1560
                $scope          = 'protected';
1561
                $scopeSpecified = true;
1562
                break;
1563
            case T_STATIC:
1564
                $isStatic = true;
1565
                break;
1566
            }
1567
        }//end for
1568
1569
        return [
1570
            'scope'           => $scope,
1571
            'scope_specified' => $scopeSpecified,
1572
            'is_static'       => $isStatic,
1573
        ];
1574
1575
    }//end getMemberProperties()
1576
1577
1578
    /**
1579
     * Returns the visibility and implementation properties of a class.
1580
     *
1581
     * The format of the array is:
1582
     * <code>
1583
     *   array(
1584
     *    'is_abstract' => false, // true if the abstract keyword was found.
1585
     *    'is_final'    => false, // true if the final keyword was found.
1586
     *   );
1587
     * </code>
1588
     *
1589
     * @param int $stackPtr The position in the stack of the T_CLASS token to
1590
     *                      acquire the properties for.
1591
     *
1592
     * @return array
1593
     * @throws \PHP_CodeSniffer\Exceptions\TokenizerException If the specified position is not a
1594
     *                                                        T_CLASS token.
1595
     */
1596
    public function getClassProperties($stackPtr)
1597
    {
1598
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS) {
1599
            throw new TokenizerException('$stackPtr must be of type T_CLASS');
1600
        }
1601
1602
        $valid = [
1603
            T_FINAL       => T_FINAL,
1604
            T_ABSTRACT    => T_ABSTRACT,
1605
            T_WHITESPACE  => T_WHITESPACE,
1606
            T_COMMENT     => T_COMMENT,
1607
            T_DOC_COMMENT => T_DOC_COMMENT,
1608
        ];
1609
1610
        $isAbstract = false;
1611
        $isFinal    = false;
1612
1613
        for ($i = ($stackPtr - 1); $i > 0; $i--) {
1614
            if (isset($valid[$this->tokens[$i]['code']]) === false) {
1615
                break;
1616
            }
1617
1618
            switch ($this->tokens[$i]['code']) {
1619
            case T_ABSTRACT:
1620
                $isAbstract = true;
1621
                break;
1622
1623
            case T_FINAL:
1624
                $isFinal = true;
1625
                break;
1626
            }
1627
        }//end for
1628
1629
        return [
1630
            'is_abstract' => $isAbstract,
1631
            'is_final'    => $isFinal,
1632
        ];
1633
1634
    }//end getClassProperties()
1635
1636
1637
    /**
1638
     * Determine if the passed token is a reference operator.
1639
     *
1640
     * Returns true if the specified token position represents a reference.
1641
     * Returns false if the token represents a bitwise operator.
1642
     *
1643
     * @param int $stackPtr The position of the T_BITWISE_AND token.
1644
     *
1645
     * @return boolean
1646
     */
1647
    public function isReference($stackPtr)
1648
    {
1649
        if ($this->tokens[$stackPtr]['code'] !== T_BITWISE_AND) {
1650
            return false;
1651
        }
1652
1653
        $tokenBefore = $this->findPrevious(
1654
            Util\Tokens::$emptyTokens,
1655
            ($stackPtr - 1),
1656
            null,
1657
            true
1658
        );
1659
1660
        if ($this->tokens[$tokenBefore]['code'] === T_FUNCTION) {
1661
            // Function returns a reference.
1662
            return true;
1663
        }
1664
1665
        if ($this->tokens[$tokenBefore]['code'] === T_DOUBLE_ARROW) {
1666
            // Inside a foreach loop or array assignment, this is a reference.
1667
            return true;
1668
        }
1669
1670
        if ($this->tokens[$tokenBefore]['code'] === T_AS) {
1671
            // Inside a foreach loop, this is a reference.
1672
            return true;
1673
        }
1674
1675
        if (isset(Util\Tokens::$assignmentTokens[$this->tokens[$tokenBefore]['code']]) === true) {
1676
            // This is directly after an assignment. It's a reference. Even if
1677
            // it is part of an operation, the other tests will handle it.
1678
            return true;
1679
        }
1680
1681
        $tokenAfter = $this->findNext(
1682
            Util\Tokens::$emptyTokens,
1683
            ($stackPtr + 1),
1684
            null,
1685
            true
1686
        );
1687
1688
        if ($this->tokens[$tokenAfter]['code'] === T_NEW) {
1689
            return true;
1690
        }
1691
1692
        if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === true) {
1693
            $brackets    = $this->tokens[$stackPtr]['nested_parenthesis'];
1694
            $lastBracket = array_pop($brackets);
1695
            if (isset($this->tokens[$lastBracket]['parenthesis_owner']) === true) {
1696
                $owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']];
1697
                if ($owner['code'] === T_FUNCTION
1698
                    || $owner['code'] === T_CLOSURE
1699
                ) {
1700
                    $params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']);
1701
                    foreach ($params as $param) {
1702
                        $varToken = $tokenAfter;
1703
                        if ($param['variable_length'] === true) {
1704
                            $varToken = $this->findNext(
1705
                                (Util\Tokens::$emptyTokens + [T_ELLIPSIS]),
1706
                                ($stackPtr + 1),
1707
                                null,
1708
                                true
1709
                            );
1710
                        }
1711
1712
                        if ($param['token'] === $varToken
1713
                            && $param['pass_by_reference'] === true
1714
                        ) {
1715
                            // Function parameter declared to be passed by reference.
1716
                            return true;
1717
                        }
1718
                    }
1719
                }//end if
1720
            } else {
1721
                $prev = false;
1722
                for ($t = ($this->tokens[$lastBracket]['parenthesis_opener'] - 1); $t >= 0; $t--) {
1723
                    if ($this->tokens[$t]['code'] !== T_WHITESPACE) {
1724
                        $prev = $t;
1725
                        break;
1726
                    }
1727
                }
1728
1729
                if ($prev !== false && $this->tokens[$prev]['code'] === T_USE) {
1730
                    // Closure use by reference.
1731
                    return true;
1732
                }
1733
            }//end if
1734
        }//end if
1735
1736
        // Pass by reference in function calls and assign by reference in arrays.
1737
        if ($this->tokens[$tokenBefore]['code'] === T_OPEN_PARENTHESIS
1738
            || $this->tokens[$tokenBefore]['code'] === T_COMMA
1739
            || $this->tokens[$tokenBefore]['code'] === T_OPEN_SHORT_ARRAY
1740
        ) {
1741
            if ($this->tokens[$tokenAfter]['code'] === T_VARIABLE) {
1742
                return true;
1743
            } else {
1744
                $skip   = Util\Tokens::$emptyTokens;
1745
                $skip[] = T_NS_SEPARATOR;
1746
                $skip[] = T_SELF;
1747
                $skip[] = T_PARENT;
1748
                $skip[] = T_STATIC;
1749
                $skip[] = T_STRING;
1750
                $skip[] = T_NAMESPACE;
1751
                $skip[] = T_DOUBLE_COLON;
1752
1753
                $nextSignificantAfter = $this->findNext(
1754
                    $skip,
1755
                    ($stackPtr + 1),
1756
                    null,
1757
                    true
1758
                );
1759
                if ($this->tokens[$nextSignificantAfter]['code'] === T_VARIABLE) {
1760
                    return true;
1761
                }
1762
            }//end if
1763
        }//end if
1764
1765
        return false;
1766
1767
    }//end isReference()
1768
1769
1770
    /**
1771
     * Returns the content of the tokens from the specified start position in
1772
     * the token stack for the specified length.
1773
     *
1774
     * @param int $start  The position to start from in the token stack.
1775
     * @param int $length The length of tokens to traverse from the start pos.
1776
     *
1777
     * @return string The token contents.
1778
     */
1779
    public function getTokensAsString($start, $length)
1780
    {
1781
        $str = '';
1782
        $end = ($start + $length);
1783
        if ($end > $this->numTokens) {
1784
            $end = $this->numTokens;
1785
        }
1786
1787
        for ($i = $start; $i < $end; $i++) {
1788
            $str .= $this->tokens[$i]['content'];
1789
        }
1790
1791
        return $str;
1792
1793
    }//end getTokensAsString()
1794
1795
1796
    /**
1797
     * Returns the position of the previous specified token(s).
1798
     *
1799
     * If a value is specified, the previous token of the specified type(s)
1800
     * containing the specified value will be returned.
1801
     *
1802
     * Returns false if no token can be found.
1803
     *
1804
     * @param int|array $types   The type(s) of tokens to search for.
1805
     * @param int       $start   The position to start searching from in the
1806
     *                           token stack.
1807
     * @param int       $end     The end position to fail if no token is found.
1808
     *                           if not specified or null, end will default to
1809
     *                           the start of the token stack.
1810
     * @param bool      $exclude If true, find the previous token that is NOT of
1811
     *                           the types specified in $types.
1812
     * @param string    $value   The value that the token(s) must be equal to.
1813
     *                           If value is omitted, tokens with any value will
1814
     *                           be returned.
1815
     * @param bool      $local   If true, tokens outside the current statement
1816
     *                           will not be checked. IE. checking will stop
1817
     *                           at the previous semi-colon found.
1818
     *
1819
     * @return int|bool
1820
     * @see    findNext()
1821
     */
1822
    public function findPrevious(
1823
        $types,
1824
        $start,
1825
        $end=null,
1826
        $exclude=false,
1827
        $value=null,
1828
        $local=false
1829
    ) {
1830
        $types = (array) $types;
1831
1832
        if ($end === null) {
1833
            $end = 0;
1834
        }
1835
1836
        for ($i = $start; $i >= $end; $i--) {
1837
            $found = (bool) $exclude;
1838
            foreach ($types as $type) {
1839
                if ($this->tokens[$i]['code'] === $type) {
1840
                    $found = !$exclude;
1841
                    break;
1842
                }
1843
            }
1844
1845
            if ($found === true) {
1846
                if ($value === null) {
1847
                    return $i;
1848
                } else if ($this->tokens[$i]['content'] === $value) {
1849
                    return $i;
1850
                }
1851
            }
1852
1853
            if ($local === true) {
1854
                if (isset($this->tokens[$i]['scope_opener']) === true
1855
                    && $i === $this->tokens[$i]['scope_closer']
1856
                ) {
1857
                    $i = $this->tokens[$i]['scope_opener'];
1858
                } else if (isset($this->tokens[$i]['bracket_opener']) === true
1859
                    && $i === $this->tokens[$i]['bracket_closer']
1860
                ) {
1861
                    $i = $this->tokens[$i]['bracket_opener'];
1862
                } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
1863
                    && $i === $this->tokens[$i]['parenthesis_closer']
1864
                ) {
1865
                    $i = $this->tokens[$i]['parenthesis_opener'];
1866
                } else if ($this->tokens[$i]['code'] === T_SEMICOLON) {
1867
                    break;
1868
                }
1869
            }
1870
        }//end for
1871
1872
        return false;
1873
1874
    }//end findPrevious()
1875
1876
1877
    /**
1878
     * Returns the position of the next specified token(s).
1879
     *
1880
     * If a value is specified, the next token of the specified type(s)
1881
     * containing the specified value will be returned.
1882
     *
1883
     * Returns false if no token can be found.
1884
     *
1885
     * @param int|array $types   The type(s) of tokens to search for.
1886
     * @param int       $start   The position to start searching from in the
1887
     *                           token stack.
1888
     * @param int       $end     The end position to fail if no token is found.
1889
     *                           if not specified or null, end will default to
1890
     *                           the end of the token stack.
1891
     * @param bool      $exclude If true, find the next token that is NOT of
1892
     *                           a type specified in $types.
1893
     * @param string    $value   The value that the token(s) must be equal to.
1894
     *                           If value is omitted, tokens with any value will
1895
     *                           be returned.
1896
     * @param bool      $local   If true, tokens outside the current statement
1897
     *                           will not be checked. i.e., checking will stop
1898
     *                           at the next semi-colon found.
1899
     *
1900
     * @return int|bool
1901
     * @see    findPrevious()
1902
     */
1903
    public function findNext(
1904
        $types,
1905
        $start,
1906
        $end=null,
1907
        $exclude=false,
1908
        $value=null,
1909
        $local=false
1910
    ) {
1911
        $types = (array) $types;
1912
1913
        if ($end === null || $end > $this->numTokens) {
1914
            $end = $this->numTokens;
1915
        }
1916
1917
        for ($i = $start; $i < $end; $i++) {
1918
            $found = (bool) $exclude;
1919
            foreach ($types as $type) {
1920
                if ($this->tokens[$i]['code'] === $type) {
1921
                    $found = !$exclude;
1922
                    break;
1923
                }
1924
            }
1925
1926
            if ($found === true) {
1927
                if ($value === null) {
1928
                    return $i;
1929
                } else if ($this->tokens[$i]['content'] === $value) {
1930
                    return $i;
1931
                }
1932
            }
1933
1934
            if ($local === true && $this->tokens[$i]['code'] === T_SEMICOLON) {
1935
                break;
1936
            }
1937
        }//end for
1938
1939
        return false;
1940
1941
    }//end findNext()
1942
1943
1944
    /**
1945
     * Returns the position of the first non-whitespace token in a statement.
1946
     *
1947
     * @param int       $start  The position to start searching from in the token stack.
1948
     * @param int|array $ignore Token types that should not be considered stop points.
1949
     *
1950
     * @return int
1951
     */
1952
    public function findStartOfStatement($start, $ignore=null)
1953
    {
1954
        $endTokens = Util\Tokens::$blockOpeners;
1955
1956
        $endTokens[T_COLON]            = true;
1957
        $endTokens[T_COMMA]            = true;
1958
        $endTokens[T_DOUBLE_ARROW]     = true;
1959
        $endTokens[T_SEMICOLON]        = true;
1960
        $endTokens[T_OPEN_TAG]         = true;
1961
        $endTokens[T_CLOSE_TAG]        = true;
1962
        $endTokens[T_OPEN_SHORT_ARRAY] = true;
1963
1964
        if ($ignore !== null) {
1965
            $ignore = (array) $ignore;
1966
            foreach ($ignore as $code) {
1967
                if (isset($endTokens[$code]) === true) {
1968
                    unset($endTokens[$code]);
1969
                }
1970
            }
1971
        }
1972
1973
        $lastNotEmpty = $start;
1974
1975
        for ($i = $start; $i >= 0; $i--) {
1976
            if (isset($endTokens[$this->tokens[$i]['code']]) === true) {
1977
                // Found the end of the previous statement.
1978
                return $lastNotEmpty;
1979
            }
1980
1981
            if (isset($this->tokens[$i]['scope_opener']) === true
1982
                && $i === $this->tokens[$i]['scope_closer']
1983
            ) {
1984
                // Found the end of the previous scope block.
1985
                return $lastNotEmpty;
1986
            }
1987
1988
            // Skip nested statements.
1989
            if (isset($this->tokens[$i]['bracket_opener']) === true
1990
                && $i === $this->tokens[$i]['bracket_closer']
1991
            ) {
1992
                $i = $this->tokens[$i]['bracket_opener'];
1993
            } else if (isset($this->tokens[$i]['parenthesis_opener']) === true
1994
                && $i === $this->tokens[$i]['parenthesis_closer']
1995
            ) {
1996
                $i = $this->tokens[$i]['parenthesis_opener'];
1997
            }
1998
1999
            if (isset(Util\Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
2000
                $lastNotEmpty = $i;
2001
            }
2002
        }//end for
2003
2004
        return 0;
2005
2006
    }//end findStartOfStatement()
2007
2008
2009
    /**
2010
     * Returns the position of the last non-whitespace token in a statement.
2011
     *
2012
     * @param int       $start  The position to start searching from in the token stack.
2013
     * @param int|array $ignore Token types that should not be considered stop points.
2014
     *
2015
     * @return int
2016
     */
2017
    public function findEndOfStatement($start, $ignore=null)
2018
    {
2019
        $endTokens = [
2020
            T_COLON                => true,
2021
            T_COMMA                => true,
2022
            T_DOUBLE_ARROW         => true,
2023
            T_SEMICOLON            => true,
2024
            T_CLOSE_PARENTHESIS    => true,
2025
            T_CLOSE_SQUARE_BRACKET => true,
2026
            T_CLOSE_CURLY_BRACKET  => true,
2027
            T_CLOSE_SHORT_ARRAY    => true,
2028
            T_OPEN_TAG             => true,
2029
            T_CLOSE_TAG            => true,
2030
        ];
2031
2032
        if ($ignore !== null) {
2033
            $ignore = (array) $ignore;
2034
            foreach ($ignore as $code) {
2035
                if (isset($endTokens[$code]) === true) {
2036
                    unset($endTokens[$code]);
2037
                }
2038
            }
2039
        }
2040
2041
        $lastNotEmpty = $start;
2042
2043
        for ($i = $start; $i < $this->numTokens; $i++) {
2044
            if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) {
2045
                // Found the end of the statement.
2046
                if ($this->tokens[$i]['code'] === T_CLOSE_PARENTHESIS
2047
                    || $this->tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET
2048
                    || $this->tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET
2049
                    || $this->tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY
2050
                    || $this->tokens[$i]['code'] === T_OPEN_TAG
2051
                    || $this->tokens[$i]['code'] === T_CLOSE_TAG
2052
                ) {
2053
                    return $lastNotEmpty;
2054
                }
2055
2056
                return $i;
2057
            }
2058
2059
            // Skip nested statements.
2060
            if (isset($this->tokens[$i]['scope_closer']) === true
2061
                && ($i === $this->tokens[$i]['scope_opener']
2062
                || $i === $this->tokens[$i]['scope_condition'])
2063
            ) {
2064
                $i = $this->tokens[$i]['scope_closer'];
2065
            } else if (isset($this->tokens[$i]['bracket_closer']) === true
2066
                && $i === $this->tokens[$i]['bracket_opener']
2067
            ) {
2068
                $i = $this->tokens[$i]['bracket_closer'];
2069
            } else if (isset($this->tokens[$i]['parenthesis_closer']) === true
2070
                && $i === $this->tokens[$i]['parenthesis_opener']
2071
            ) {
2072
                $i = $this->tokens[$i]['parenthesis_closer'];
2073
            }
2074
2075
            if (isset(Util\Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) {
2076
                $lastNotEmpty = $i;
2077
            }
2078
        }//end for
2079
2080
        return ($this->numTokens - 1);
2081
2082
    }//end findEndOfStatement()
2083
2084
2085
    /**
2086
     * Returns the position of the first token on a line, matching given type.
2087
     *
2088
     * Returns false if no token can be found.
2089
     *
2090
     * @param int|array $types   The type(s) of tokens to search for.
2091
     * @param int       $start   The position to start searching from in the
2092
     *                           token stack. The first token matching on
2093
     *                           this line before this token will be returned.
2094
     * @param bool      $exclude If true, find the token that is NOT of
2095
     *                           the types specified in $types.
2096
     * @param string    $value   The value that the token must be equal to.
2097
     *                           If value is omitted, tokens with any value will
2098
     *                           be returned.
2099
     *
2100
     * @return int | bool
2101
     */
2102
    public function findFirstOnLine($types, $start, $exclude=false, $value=null)
2103
    {
2104
        if (is_array($types) === false) {
2105
            $types = [$types];
2106
        }
2107
2108
        $foundToken = false;
2109
2110
        for ($i = $start; $i >= 0; $i--) {
2111
            if ($this->tokens[$i]['line'] < $this->tokens[$start]['line']) {
2112
                break;
2113
            }
2114
2115
            $found = $exclude;
2116
            foreach ($types as $type) {
2117
                if ($exclude === false) {
2118
                    if ($this->tokens[$i]['code'] === $type) {
2119
                        $found = true;
2120
                        break;
2121
                    }
2122
                } else {
2123
                    if ($this->tokens[$i]['code'] === $type) {
2124
                        $found = false;
2125
                        break;
2126
                    }
2127
                }
2128
            }
2129
2130
            if ($found === true) {
2131
                if ($value === null) {
2132
                    $foundToken = $i;
2133
                } else if ($this->tokens[$i]['content'] === $value) {
2134
                    $foundToken = $i;
2135
                }
2136
            }
2137
        }//end for
2138
2139
        return $foundToken;
2140
2141
    }//end findFirstOnLine()
2142
2143
2144
    /**
2145
     * Determine if the passed token has a condition of one of the passed types.
2146
     *
2147
     * @param int       $stackPtr The position of the token we are checking.
2148
     * @param int|array $types    The type(s) of tokens to search for.
2149
     *
2150
     * @return boolean
2151
     */
2152
    public function hasCondition($stackPtr, $types)
2153
    {
2154
        // Check for the existence of the token.
2155
        if (isset($this->tokens[$stackPtr]) === false) {
2156
            return false;
2157
        }
2158
2159
        // Make sure the token has conditions.
2160
        if (isset($this->tokens[$stackPtr]['conditions']) === false) {
2161
            return false;
2162
        }
2163
2164
        $types      = (array) $types;
2165
        $conditions = $this->tokens[$stackPtr]['conditions'];
2166
2167
        foreach ($types as $type) {
2168
            if (in_array($type, $conditions) === true) {
2169
                // We found a token with the required type.
2170
                return true;
2171
            }
2172
        }
2173
2174
        return false;
2175
2176
    }//end hasCondition()
2177
2178
2179
    /**
2180
     * Return the position of the condition for the passed token.
2181
     *
2182
     * Returns FALSE if the token does not have the condition.
2183
     *
2184
     * @param int $stackPtr The position of the token we are checking.
2185
     * @param int $type     The type of token to search for.
2186
     *
2187
     * @return int
2188
     */
2189
    public function getCondition($stackPtr, $type)
2190
    {
2191
        // Check for the existence of the token.
2192
        if (isset($this->tokens[$stackPtr]) === false) {
2193
            return false;
2194
        }
2195
2196
        // Make sure the token has conditions.
2197
        if (isset($this->tokens[$stackPtr]['conditions']) === false) {
2198
            return false;
2199
        }
2200
2201
        $conditions = $this->tokens[$stackPtr]['conditions'];
2202
        foreach ($conditions as $token => $condition) {
2203
            if ($condition === $type) {
2204
                return $token;
2205
            }
2206
        }
2207
2208
        return false;
2209
2210
    }//end getCondition()
2211
2212
2213
    /**
2214
     * Returns the name of the class that the specified class extends.
2215
     * (works for classes, anonymous classes and interfaces)
2216
     *
2217
     * Returns FALSE on error or if there is no extended class name.
2218
     *
2219
     * @param int $stackPtr The stack position of the class.
2220
     *
2221
     * @return string|false
2222
     */
2223
    public function findExtendedClassName($stackPtr)
2224
    {
2225
        // Check for the existence of the token.
2226
        if (isset($this->tokens[$stackPtr]) === false) {
2227
            return false;
2228
        }
2229
2230
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
2231
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
2232
            && $this->tokens[$stackPtr]['code'] !== T_INTERFACE
2233
        ) {
2234
            return false;
2235
        }
2236
2237
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
2238
            return false;
2239
        }
2240
2241
        $classCloserIndex = $this->tokens[$stackPtr]['scope_closer'];
2242
        $extendsIndex     = $this->findNext(T_EXTENDS, $stackPtr, $classCloserIndex);
2243
        if (false === $extendsIndex) {
2244
            return false;
2245
        }
2246
2247
        $find = [
2248
            T_NS_SEPARATOR,
2249
            T_STRING,
2250
            T_WHITESPACE,
2251
        ];
2252
2253
        $end  = $this->findNext($find, ($extendsIndex + 1), $classCloserIndex, true);
2254
        $name = $this->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1));
2255
        $name = trim($name);
2256
2257
        if ($name === '') {
2258
            return false;
2259
        }
2260
2261
        return $name;
2262
2263
    }//end findExtendedClassName()
2264
2265
2266
    /**
2267
     * Returns the names of the interfaces that the specified class implements.
2268
     *
2269
     * Returns FALSE on error or if there are no implemented interface names.
2270
     *
2271
     * @param int $stackPtr The stack position of the class.
2272
     *
2273
     * @return array|false
2274
     */
2275
    public function findImplementedInterfaceNames($stackPtr)
2276
    {
2277
        // Check for the existence of the token.
2278
        if (isset($this->tokens[$stackPtr]) === false) {
2279
            return false;
2280
        }
2281
2282
        if ($this->tokens[$stackPtr]['code'] !== T_CLASS
2283
            && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS
2284
        ) {
2285
            return false;
2286
        }
2287
2288
        if (isset($this->tokens[$stackPtr]['scope_closer']) === false) {
2289
            return false;
2290
        }
2291
2292
        $classOpenerIndex = $this->tokens[$stackPtr]['scope_opener'];
2293
        $implementsIndex  = $this->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex);
2294
        if ($implementsIndex === false) {
2295
            return false;
2296
        }
2297
2298
        $find = [
2299
            T_NS_SEPARATOR,
2300
            T_STRING,
2301
            T_WHITESPACE,
2302
            T_COMMA,
2303
        ];
2304
2305
        $end  = $this->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true);
2306
        $name = $this->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1));
2307
        $name = trim($name);
2308
2309
        if ($name === '') {
2310
            return false;
2311
        } else {
2312
            $names = explode(',', $name);
2313
            $names = array_map('trim', $names);
2314
            return $names;
2315
        }
2316
2317
    }//end findImplementedInterfaceNames()
2318
2319
2320
}//end class
2321